├── .github
├── FUNDING.yml
└── workflows
│ ├── docc.yml
│ ├── macOS.yml
│ ├── ubuntu.yml
│ └── windows.yml
├── .gitignore
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── AppState
│ ├── Application
│ ├── Application+internal.swift
│ ├── Application+public.swift
│ ├── Application.swift
│ └── Types
│ │ ├── Dependency
│ │ ├── Application+Dependency.swift
│ │ └── Slice
│ │ │ └── Application+DependencySlice.swift
│ │ ├── Helper
│ │ ├── Application+ApplicationPreview.swift
│ │ ├── Application+Scope.swift
│ │ ├── ApplicationLogger.swift
│ │ ├── FileManager+AppState.swift
│ │ ├── FileManaging.swift
│ │ ├── Loggable.swift
│ │ ├── MutableApplicationState.swift
│ │ ├── UbiquitousKeyValueStoreManaging.swift
│ │ └── UserDefaultsManaging.swift
│ │ └── State
│ │ ├── Application+FileState.swift
│ │ ├── Application+SecureState.swift
│ │ ├── Application+State.swift
│ │ ├── Application+StoredState.swift
│ │ ├── Application+SyncState.swift
│ │ └── Slice
│ │ ├── Application+OptionalSlice.swift
│ │ ├── Application+OptionalSliceOptionalValue.swift
│ │ └── Application+Slice.swift
│ ├── Dependencies
│ └── Keychain.swift
│ └── PropertyWrappers
│ ├── Dependency
│ ├── AppDependency.swift
│ ├── ObservedDependency.swift
│ └── Slice
│ │ ├── DependencyConstant.swift
│ │ └── DependencySlice.swift
│ └── State
│ ├── AppState.swift
│ ├── FileState.swift
│ ├── SecureState.swift
│ ├── Slice
│ ├── Constant.swift
│ ├── OptionalConstant.swift
│ ├── OptionalSlice.swift
│ └── Slice.swift
│ ├── StoredState.swift
│ └── SyncState.swift
├── Tests
└── AppStateTests
│ ├── AppDependencyTests.swift
│ ├── AppStateTests.swift
│ ├── ApplicationTests.swift
│ ├── DependencySliceTests.swift
│ ├── FileStateTests.swift
│ ├── KeychainTests.swift
│ ├── ObservedDependencyTests.swift
│ ├── OptionalSliceTests.swift
│ ├── SecureStateTests.swift
│ ├── SliceTests.swift
│ ├── StoredStateTests.swift
│ └── SyncStateTests.swift
└── documentation
├── advanced-usage.md
├── best-practices.md
├── contributing.md
├── installation.md
├── migration-considerations.md
├── syncstate-implementation.md
├── usage-constant.md
├── usage-filestate.md
├── usage-observeddependency.md
├── usage-overview.md
├── usage-securestate.md
├── usage-slice.md
├── usage-state-dependency.md
├── usage-storedstate.md
└── usage-syncstate.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [0xLeif]
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/workflows/docc.yml:
--------------------------------------------------------------------------------
1 | name: docc
2 | on:
3 | push:
4 | branches: [ "main" ]
5 | permissions:
6 | contents: read
7 | pages: write
8 | id-token: write
9 | concurrency:
10 | group: pages
11 | cancel-in-progress: true
12 | jobs:
13 | pages:
14 | environment:
15 | name: github-pages
16 | url: '${{ steps.deployment.outputs.page_url }}'
17 | runs-on: macos-latest
18 | steps:
19 | - uses: swift-actions/setup-swift@v1
20 | - name: git checkout
21 | uses: actions/checkout@v3
22 | - name: docbuild
23 | run: >
24 | sudo xcode-select -s /Applications/Xcode_16.0.app;
25 | xcodebuild docbuild -scheme AppState \
26 | -derivedDataPath /tmp/docbuild \
27 | -destination 'generic/platform=iOS';
28 | $(xcrun --find docc) process-archive \
29 | transform-for-static-hosting /tmp/docbuild/Build/Products/Debug-iphoneos/AppState.doccarchive \
30 | --output-path docs \
31 | --hosting-base-path 'AppState';
32 | echo "" > docs/index.html;
34 | - name: artifacts
35 | uses: actions/upload-pages-artifact@v1
36 | with:
37 | path: docs
38 | - name: deploy
39 | id: deployment
40 | uses: actions/deploy-pages@v1
41 |
--------------------------------------------------------------------------------
/.github/workflows/macOS.yml:
--------------------------------------------------------------------------------
1 | name: macOS
2 |
3 | on:
4 | push:
5 | branches: ["**"]
6 |
7 | jobs:
8 | build:
9 | runs-on: macos-latest
10 | steps:
11 | - uses: maxim-lobanov/setup-xcode@v1
12 | with:
13 | xcode-version: 16.0
14 | - uses: actions/checkout@v3
15 | - name: Build
16 | run: swift build -v
17 | - name: Run tests
18 | run: swift test -v
19 |
--------------------------------------------------------------------------------
/.github/workflows/ubuntu.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Swift project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift
3 |
4 | name: Ubuntu
5 |
6 | on:
7 | push:
8 | branches: ["**"]
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-20.04
13 |
14 | steps:
15 | - uses: sersoft-gmbh/swifty-linux-action@v3
16 | with:
17 | release-version: 6.0.1
18 | - uses: actions/checkout@v3
19 | - name: Build for release
20 | run: swift build -v -c release
21 | - name: Test
22 | run: swift test -v
23 |
--------------------------------------------------------------------------------
/.github/workflows/windows.yml:
--------------------------------------------------------------------------------
1 | name: Windows
2 |
3 | on:
4 | push:
5 | branches: ["**"]
6 |
7 | jobs:
8 | build:
9 | runs-on: windows-latest
10 | steps:
11 | - uses: compnerd/gha-setup-swift@main
12 | with:
13 | branch: swift-6.0-release
14 | tag: 6.0-RELEASE
15 |
16 | - uses: actions/checkout@v2
17 | - run: swift build
18 | - run: swift test
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/
7 | .netrc
8 | Package.resolved
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Zach Eriksen
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 PackageDescription
4 |
5 | let package = Package(
6 | name: "AppState",
7 | platforms: [
8 | .iOS(.v15),
9 | .watchOS(.v8),
10 | .macOS(.v11),
11 | .tvOS(.v15),
12 | .visionOS(.v1)
13 | ],
14 | products: [
15 | .library(
16 | name: "AppState",
17 | targets: ["AppState"]
18 | )
19 | ],
20 | dependencies: [
21 | .package(url: "https://github.com/0xLeif/Cache", from: "2.0.0")
22 | ],
23 | targets: [
24 | .target(
25 | name: "AppState",
26 | dependencies: [
27 | "Cache"
28 | ]
29 | ),
30 | .testTarget(
31 | name: "AppStateTests",
32 | dependencies: ["AppState"]
33 | )
34 | ]
35 | )
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AppState
2 |
3 | [](https://github.com/0xLeif/AppState/actions/workflows/macOS.yml)
4 | [](https://github.com/0xLeif/AppState/actions/workflows/ubuntu.yml)
5 | [](https://github.com/0xLeif/AppState/actions/workflows/windows.yml)
6 | [](https://github.com/0xLeif/AppState/blob/main/LICENSE)
7 | [](https://github.com/0xLeif/AppState/releases)
8 |
9 | **AppState** is a Swift 6 library designed to simplify the management of application state in a thread-safe, type-safe, and SwiftUI-friendly way. It provides a set of tools to centralize and synchronize state across your application, as well as inject dependencies into various parts of your app.
10 |
11 | ## Requirements
12 |
13 | - **iOS**: 15.0+
14 | - **watchOS**: 8.0+
15 | - **macOS**: 11.0+
16 | - **tvOS**: 15.0+
17 | - **visionOS**: 1.0+
18 | - **Swift**: 6.0+
19 | - **Xcode**: 16.0+
20 |
21 | **Non-Apple Platform Support**: Linux & Windows
22 |
23 | > 🍎 Features marked with this symbol are specific to Apple platforms, as they rely on Apple technologies such as iCloud and the Keychain.
24 |
25 | ## Key Features
26 |
27 | **AppState** includes several powerful features to help manage state and dependencies:
28 |
29 | - **State**: Centralized state management that allows you to encapsulate and broadcast changes across the app.
30 | - **StoredState**: Persistent state using `UserDefaults`, ideal for saving small amounts of data between app launches.
31 | - **FileState**: Persistent state stored using `FileManager`, useful for storing larger amounts of data securely on disk.
32 | - 🍎 **SyncState**: Synchronize state across multiple devices using iCloud, ensuring consistency in user preferences and settings.
33 | - 🍎 **SecureState**: Store sensitive data securely using the Keychain, protecting user information such as tokens or passwords.
34 | - **Dependency Management**: Inject dependencies like network services or database clients across your app for better modularity and testing.
35 | - **Slicing**: Access specific parts of a state or dependency for granular control without needing to manage the entire application state.
36 |
37 | ## Getting Started
38 |
39 | To integrate **AppState** into your Swift project, you’ll need to use the Swift Package Manager. Follow the [Installation Guide](documentation/installation.md) for detailed instructions on setting up **AppState**.
40 |
41 | After installation, refer to the [Usage Overview](documentation/usage-overview.md) for a quick introduction on how to manage state and inject dependencies into your project.
42 |
43 | ## Documentation
44 |
45 | Here’s a detailed breakdown of **AppState**'s documentation:
46 |
47 | - [Installation Guide](documentation/installation.md): How to add **AppState** to your project using Swift Package Manager.
48 | - [Usage Overview](documentation/usage-overview.md): An overview of key features with example implementations.
49 |
50 | ### Detailed Usage Guides:
51 |
52 | - [State and Dependency Management](documentation/usage-state-dependency.md): Centralize state and inject dependencies throughout your app.
53 | - [Slicing State](documentation/usage-slice.md): Access and modify specific parts of the state.
54 | - [StoredState Usage Guide](documentation/usage-storedstate.md): How to persist lightweight data using `StoredState`.
55 | - [FileState Usage Guide](documentation/usage-filestate.md): Learn how to persist larger amounts of data securely on disk.
56 | - [Keychain SecureState Usage](documentation/usage-securestate.md): Store sensitive data securely using the Keychain.
57 | - [iCloud Syncing with SyncState](documentation/usage-syncstate.md): Keep state synchronized across devices using iCloud.
58 |
59 | ## Contributing
60 |
61 | We welcome contributions! Please check out our [Contributing Guide](documentation/contributing.md) for how to get involved.
62 |
63 | ## Next Steps
64 |
65 | With **AppState** installed, you can start exploring its key features by checking out the [Usage Overview](documentation/usage-overview.md) and more detailed guides. Get started with managing state and dependencies effectively in your Swift projects! For more advanced usage techniques, like Just-In-Time creation and preloading dependencies, see the [Advanced Usage Guide](documentation/advanced-usage.md).
66 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Application+internal.swift:
--------------------------------------------------------------------------------
1 | extension Application {
2 | @MainActor
3 | static var cacheDescription: String {
4 | shared.cache.allValues
5 | .map { key, value in
6 | if let value = value as? Loggable {
7 | "\t- \(value.logValue)"
8 | } else {
9 | "\t- \(value)"
10 | }
11 | }
12 | .sorted(by: <)
13 | .joined(separator: "\n")
14 | }
15 |
16 | /**
17 | Generates a specific identifier string for given code context
18 |
19 | - Parameters:
20 | - fileID: The file identifier of the code, generally provided by `#fileID` directive.
21 | - function: The function name of the code, generally provided by `#function` directive.
22 | - line: The line number of the code, generally provided by `#line` directive.
23 | - column: The column number of the code, generally provided by `#column` directive.
24 | - Returns: A string representing the specific location and context of the code. The format is `[@|]`.
25 | */
26 | static func codeID(
27 | fileID: StaticString,
28 | function: StaticString,
29 | line: Int,
30 | column: Int
31 | ) -> String {
32 | "\(fileID)[\(function)@\(line)|\(column)]"
33 | }
34 |
35 | /// Internal log function.
36 | @MainActor
37 | static func log(
38 | debug message: String,
39 | fileID: StaticString,
40 | function: StaticString,
41 | line: Int,
42 | column: Int
43 | ) {
44 | log(
45 | debug: { message },
46 | fileID: fileID,
47 | function: function,
48 | line: line,
49 | column: column
50 | )
51 | }
52 |
53 | /// Internal log function.
54 | @MainActor
55 | static func log(
56 | debug message: () -> String,
57 | fileID: StaticString,
58 | function: StaticString,
59 | line: Int,
60 | column: Int
61 | ) {
62 | guard isLoggingEnabled else { return }
63 |
64 | let excludedFileIDs: [String] = [
65 | "AppState/Application+StoredState.swift",
66 | "AppState/Application+SyncState.swift",
67 | "AppState/Application+SecureState.swift",
68 | "AppState/Application+Slice.swift",
69 | "AppState/Application+FileState.swift",
70 | ]
71 | let isFileIDValue: Bool = excludedFileIDs.contains(fileID.description) == false
72 |
73 | guard isFileIDValue else { return }
74 |
75 | let debugMessage = message()
76 | let codeID = codeID(fileID: fileID, function: function, line: line, column: column)
77 |
78 | logger.debug("\(debugMessage) (\(codeID))")
79 | }
80 |
81 | /// Internal log function.
82 | @MainActor
83 | static func log(
84 | error: Error,
85 | message: String,
86 | fileID: StaticString,
87 | function: StaticString,
88 | line: Int,
89 | column: Int
90 | ) {
91 | guard isLoggingEnabled else { return }
92 |
93 | let codeID = codeID(fileID: fileID, function: function, line: line, column: column)
94 |
95 | logger.error(
96 | """
97 | \(message) Error: {
98 | ❌ \(error)
99 | } (\(codeID))
100 | """
101 | )
102 | }
103 |
104 | /// Returns value for the provided keyPath. This method is thread safe
105 | ///
106 | /// - Parameter keyPath: KeyPath of the value to be fetched
107 | func value(keyPath: KeyPath) -> Value {
108 | lock.lock(); defer { lock.unlock() }
109 |
110 | return self[keyPath: keyPath]
111 | }
112 |
113 | /**
114 | Use this function to make sure Dependencies are intialized. If a Dependency is not loaded, it will be initialized whenever it is used next.
115 |
116 | - Parameter dependency: KeyPath of the Dependency to be loaded
117 | */
118 | func load(
119 | dependency keyPath: KeyPath>
120 | ) {
121 | _ = value(keyPath: keyPath)
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Application.swift:
--------------------------------------------------------------------------------
1 | import Cache
2 | #if !os(Linux) && !os(Windows)
3 | import Combine
4 | import OSLog
5 | #else
6 | import Foundation
7 | #endif
8 |
9 | /// `Application` is a class that can be observed for changes, keeping track of the states within the application.
10 | open class Application: NSObject {
11 | /// Singleton shared instance of `Application`
12 | @MainActor
13 | static var shared: Application = Application()
14 |
15 | #if !os(Linux) && !os(Windows)
16 | /// Logger specifically for AppState
17 | public static let logger: Logger = Logger(subsystem: "AppState", category: "Application")
18 | #else
19 | /// Logger specifically for AppState
20 | @MainActor
21 | public static var logger: ApplicationLogger = ApplicationLogger()
22 | #endif
23 |
24 | @MainActor
25 | static var isLoggingEnabled: Bool = false
26 |
27 | let lock: NSRecursiveLock
28 |
29 | /// Cache to store values
30 | let cache: Cache
31 |
32 | #if !os(Linux) && !os(Windows)
33 | private var bag: Set = Set()
34 |
35 | deinit { bag.removeAll() }
36 | #endif
37 |
38 | /// Default init used as the default Application, but also any custom implementation of Application. You should never call this function, but instead should use `Application.promote(to: CustomApplication.self)`.
39 | public required init(
40 | setup: (Application) -> Void = { _ in }
41 | ) {
42 | lock = NSRecursiveLock()
43 | cache = Cache()
44 |
45 | super.init()
46 |
47 | setup(self)
48 | loadDefaultDependencies()
49 |
50 | #if !os(Linux) && !os(Windows)
51 | consume(object: cache)
52 | #endif
53 | }
54 |
55 | #if !os(Linux) && !os(Windows)
56 | /**
57 | Called when the value of one or more keys in the local key-value store changed due to incoming data pushed from iCloud.
58 |
59 | This notification is sent only upon a change received from iCloud; it is not sent when your app sets a value.
60 |
61 | The user info dictionary can contain the reason for the notification as well as a list of which values changed, as follows:
62 | - The value of the ``NSUbiquitousKeyValueStoreChangeReasonKey`` key, when present, indicates why the key-value store changed. Its value is one of the constants in Change Reason Values.
63 | - The value of the ``NSUbiquitousKeyValueStoreChangedKeysKey``, when present, is an array of strings, each the name of a key whose value changed.
64 |
65 | The notification object is the ``NSUbiquitousKeyValueStore`` object whose contents changed.
66 |
67 | Changes you make to the key-value store are saved to memory. The system then synchronizes the in-memory keys and values with the local on-disk cache, automatically and at appropriate times. For example, it synchronizes the keys when your app is put into the background, when changes are received from iCloud, and when your app makes changes to a key but does not call the synchronize() method for several seconds.
68 |
69 | - Note: Calling `Application.dependency(\.icloudStore).synchronize()` does not force new keys and values to be written to iCloud. Rather, it lets iCloud know that new keys and values are available to be uploaded. Do not rely on your keys and values being available on other devices immediately. The system controls when those keys and values are uploaded. The frequency of upload requests for key-value storage is limited to several per minute.
70 | */
71 | @MainActor
72 | @objc @available(watchOS 9.0, *)
73 | open func didChangeExternally(notification: Notification) {
74 | Application.log(
75 | debug: """
76 | ☁️ SyncState was changed externally {
77 | \(dump(notification))
78 | }
79 | """,
80 | fileID: #fileID,
81 | function: #function,
82 | line: #line,
83 | column: #column
84 | )
85 | }
86 | #endif
87 |
88 | /// Loads the default dependencies for use in Application.
89 | private func loadDefaultDependencies() {
90 | load(dependency: \.userDefaults)
91 | load(dependency: \.fileManager)
92 | }
93 |
94 | #if !os(Linux) && !os(Windows)
95 | /// Consumes changes in the provided ObservableObject and sends updates before the object will change.
96 | ///
97 | /// - Parameter object: The ObservableObject to observe
98 | private func consume(
99 | object: Object
100 | ) where ObjectWillChangePublisher == ObservableObjectPublisher {
101 | bag.insert(
102 | object.objectWillChange.sink(
103 | receiveCompletion: { _ in },
104 | receiveValue: { [weak self] _ in
105 | self?.objectWillChange.send()
106 | }
107 | )
108 | )
109 | }
110 | #endif
111 | }
112 |
113 | #if !os(Linux) && !os(Windows)
114 | extension Application: ObservableObject { }
115 | #endif
116 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/Dependency/Application+Dependency.swift:
--------------------------------------------------------------------------------
1 | extension Application {
2 | var dependencyPromotions: State<[DependencyOverride]> {
3 | state(initial: [])
4 | }
5 |
6 | /// `Dependency` struct encapsulates dependencies used throughout the app.
7 | public struct Dependency: Sendable, Loggable {
8 | /// The dependency value.
9 | var value: Value
10 |
11 | /// The scope in which this state exists.
12 | let scope: Scope
13 |
14 | /**
15 | Initializes a new dependency within a given scope with an initial value.
16 |
17 | A Dependency allows for defining services or shared objects across the app. It is designed to be read-only and can only be changed by re-initializing it, ensuring thread-safety in your app.
18 |
19 | - Parameters:
20 | - value: The initial value of the dependency.
21 | - scope: The scope in which the dependency exists.
22 | */
23 | init(
24 | _ value: Value,
25 | scope: Scope
26 | ) {
27 | self.value = value
28 | self.scope = scope
29 | }
30 |
31 | public var logValue: String {
32 | "Dependency<\(Value.self)>(\(value)) (\(scope.key))"
33 | }
34 | }
35 |
36 | /// `DependencyOverride` provides a handle to revert a dependency override operation.
37 | public final class DependencyOverride: Sendable {
38 | /// Closure to be invoked when the dependency override is cancelled. This closure typically contains logic to revert the overrides on the dependency.
39 | private let cancelOverride: @Sendable () async -> Void
40 |
41 | /**
42 | Initializes a `DependencyOverride` instance.
43 |
44 | - Parameter cancelOverride: The closure to be invoked when the
45 | dependency override is cancelled.
46 | */
47 | init(cancelOverride: @Sendable @escaping () async -> Void) {
48 | self.cancelOverride = cancelOverride
49 | }
50 |
51 | /// Cancels the override and resets the Dependency back to its value before the override.
52 | public func cancel() async {
53 | await cancelOverride()
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/Dependency/Slice/Application+DependencySlice.swift:
--------------------------------------------------------------------------------
1 | extension Application {
2 | /// `DependencySlice` allows access and modification to a specific part of an AppState's dependencies. Supports `Dependency`.
3 | public struct DependencySlice<
4 | Value: Sendable,
5 | SliceValue,
6 | SliceKeyPath: KeyPath
7 | > {
8 | /// A private backing storage for the dependency.
9 | private var dependency: Dependency
10 | private let keyPath: SliceKeyPath
11 |
12 | @MainActor
13 | init(
14 | _ stateKeyPath: KeyPath>,
15 | value valueKeyPath: SliceKeyPath
16 | ) {
17 | self.dependency = shared.value(keyPath: stateKeyPath)
18 | self.keyPath = valueKeyPath
19 | }
20 | }
21 | }
22 |
23 | extension Application.DependencySlice where SliceKeyPath == KeyPath {
24 | /// The current dependency value.
25 | @MainActor
26 | public var value: SliceValue {
27 | dependency.value[keyPath: keyPath]
28 | }
29 | }
30 |
31 | extension Application.DependencySlice where SliceKeyPath == WritableKeyPath {
32 | /// The current dependency value.
33 | @MainActor
34 | public var value: SliceValue {
35 | get { dependency.value[keyPath: keyPath] }
36 | set {
37 | #if !os(Linux) && !os(Windows)
38 | Application.shared.objectWillChange.send()
39 | #endif
40 | dependency.value[keyPath: keyPath] = newValue
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/Helper/Application+ApplicationPreview.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import SwiftUI
3 |
4 | extension Application {
5 | struct ApplicationPreview: View {
6 | let dependencyOverrides: [DependencyOverride]
7 | let content: () -> Content
8 |
9 | init(
10 | dependencyOverrides: [DependencyOverride],
11 | content: @escaping () -> Content
12 | ) {
13 | self.dependencyOverrides = dependencyOverrides
14 | self.content = content
15 | }
16 |
17 | var body: some View {
18 | content()
19 | }
20 | }
21 | }
22 | #endif
23 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/Helper/Application+Scope.swift:
--------------------------------------------------------------------------------
1 | extension Application {
2 | /**
3 | `Scope` represents a specific context in your application, defined by a name and an id. It's mainly used to maintain a specific state or behavior in your application.
4 |
5 | For example, it could be used to scope a state to a particular screen or user interaction flow.
6 | */
7 | public struct Scope: Sendable {
8 | /// The name of the scope context
9 | public let name: String
10 |
11 | /// The specific id for this scope context
12 | public let id: String
13 |
14 | /// Key computed property which builds a unique key for a given scope by combining `name` and `id` separated by "/"
15 | public var key: String {
16 | "\(name)/\(id)"
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/Helper/ApplicationLogger.swift:
--------------------------------------------------------------------------------
1 | #if os(Linux) || os(Windows)
2 | import Foundation
3 |
4 | /// `ApplicationLogger` is a struct that provides logging functionalities for Linux and Windows operating systems using closures.
5 | public struct ApplicationLogger: Sendable {
6 | public enum LoggerError: Error {
7 | case generalError
8 | }
9 |
10 | private var debugClosure: @Sendable (String) -> Void
11 | private var errorClosure: @Sendable (Error, String?) -> Void
12 |
13 | /// Initializes the `ApplicationLogger` struct with custom behaviors for each closure.
14 | /// - Parameters:
15 | /// - debug: A closure for logging debug messages.
16 | /// - debugWithClosure: A closure for logging debug messages using a closure.
17 | /// - errorWithMessage: A closure for logging error messages with an optional custom message.
18 | /// - errorWithString: A closure for logging error messages as strings.
19 | public init(
20 | debug: @Sendable @escaping (String) -> Void = { print($0) },
21 | error: @Sendable @escaping (Error, String?) -> Void = { error, message in
22 | if let message = message {
23 | print("\(message) (Error: \(error.localizedDescription))")
24 | } else {
25 | print("Error: \(error.localizedDescription)")
26 | }
27 | }
28 | ) {
29 | self.debugClosure = debug
30 | self.errorClosure = error
31 | }
32 |
33 | /// Prints a debug message.
34 | /// - Parameter message: The message to be logged.
35 | public func debug(_ message: String) {
36 | debugClosure(message)
37 | }
38 |
39 | /// Prints a debug message using a closure.
40 | /// - Parameter message: A closure that returns the message to be logged.
41 | public func debug(_ message: () -> String) {
42 | debug(message())
43 | }
44 |
45 | /// Logs an error message.
46 | /// - Parameters:
47 | /// - error: The error that occurred.
48 | /// - message: An optional custom message to accompany the error.
49 | public func error(_ error: Error, _ message: String? = nil) {
50 | errorClosure(error, message)
51 | }
52 |
53 | /// Logs a general error message.
54 | /// - Parameters:
55 | /// - message: An custom message to accompany the error.
56 | public func error(_ message: String) {
57 | errorClosure(LoggerError.generalError, message)
58 | }
59 | }
60 |
61 | #endif
62 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/Helper/FileManager+AppState.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension FileManager {
4 | /// Gets the documentDirectory from FileManager and appends "/App". Otherwise if it can not get the documents directory is will return "~/App". This variable can be set to whatever path you want to be the default.
5 | @MainActor
6 | public static var defaultFileStatePath: String = {
7 | guard let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
8 | return "~/App"
9 | }
10 |
11 | #if !os(Linux) && !os(Windows)
12 | if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
13 | return path.appending(path: "App").path()
14 | } else {
15 | return "\(path.path)/App"
16 | }
17 | #else
18 | return "\(path.path)/App"
19 | #endif
20 | }()
21 |
22 | /// Creates a directory at the specified path.
23 | ///
24 | /// - Parameters:
25 | /// - path: The path at which to create the directory.
26 | /// - Throws: An error if the directory could not be created.
27 | func createDirectory(path: String) throws {
28 | try createDirectory(atPath: path, withIntermediateDirectories: true)
29 | }
30 |
31 | /// Read a file's data as the type `Value`
32 | ///
33 | /// - Parameters:
34 | /// - path: The path to the directory containing the file. The default is `.`, which means the current working directory.
35 | /// - filename: The name of the file to read.
36 | /// - Returns: The file's data decoded as an instance of `Value`.
37 | /// - Throws: If there's an error reading the file or decoding its data.
38 | func `in`(
39 | path: String = ".",
40 | filename: String
41 | ) throws -> Value {
42 | let data = try data(
43 | path: path,
44 | filename: filename
45 | )
46 |
47 | return try JSONDecoder().decode(Value.self, from: data)
48 | }
49 |
50 | /// Read a file's data
51 | ///
52 | /// - Parameters:
53 | /// - path: The path to the directory containing the file. The default is `.`, which means the current working directory.
54 | /// - filename: The name of the file to read.
55 | /// - Returns: The file's data.
56 | /// - Throws: If there's an error reading the file.
57 | func data(
58 | path: String = ".",
59 | filename: String
60 | ) throws -> Data {
61 | let directoryURL: URL = url(filePath: path)
62 |
63 | let fileData = try Data(
64 | contentsOf: directoryURL.appendingPathComponent(filename)
65 | )
66 |
67 | guard let base64DecodedData = Data(base64Encoded: fileData) else {
68 | return fileData
69 | }
70 |
71 | return base64DecodedData
72 | }
73 |
74 | /// Write data to a file using a JSONEncoder
75 | ///
76 | /// - Parameters:
77 | /// - value: The data to write to the file. It must conform to the `Encodable` protocol.
78 | /// - path: The path to the directory where the file should be written. The default is `.`, which means the current working directory.
79 | /// - filename: The name of the file to write.
80 | /// - base64Encoded: A Boolean value indicating whether the data should be Base64-encoded before writing to the file. The default is `true`.
81 | /// - Throws: If there's an error writing the data to the file.
82 | func out(
83 | _ value: Value,
84 | path: String = ".",
85 | filename: String,
86 | base64Encoded: Bool = true
87 | ) throws {
88 | let data = try JSONEncoder().encode(value)
89 |
90 | try out(
91 | data: data,
92 | path: path,
93 | filename: filename,
94 | base64Encoded: base64Encoded
95 | )
96 | }
97 |
98 | /// Write data to a file
99 | ///
100 | /// - Parameters:
101 | /// - value: The data to write to the file.
102 | /// - path: The path to the directory where the file should be written. The default is `.`, which means the current working directory.
103 | /// - filename: The name of the file to write.
104 | /// - base64Encoded: A Boolean value indicating whether the data should be Base64-encoded before writing to the file. The default is `true`.
105 | /// - Throws: If there's an error writing the data to the file.
106 | func out(
107 | data: Data,
108 | path: String = ".",
109 | filename: String,
110 | base64Encoded: Bool = true
111 | ) throws {
112 | let directoryURL: URL = url(filePath: path)
113 |
114 | var data = data
115 |
116 | if base64Encoded {
117 | data = data.base64EncodedData()
118 | }
119 |
120 | if fileExists(atPath: path) == false {
121 | try createDirectory(path: path)
122 | }
123 |
124 | try data.write(
125 | to: directoryURL.appendingPathComponent(filename)
126 | )
127 | }
128 |
129 | /// Delete a file
130 | ///
131 | /// - Parameters:
132 | /// - path: The path to the directory containing the file. The default is `.`, which means the current working directory.
133 | /// - filename: The name of the file to delete.
134 | /// - Throws: If there's an error deleting the file.
135 | func delete(
136 | path: String = ".",
137 | filename: String
138 | ) throws {
139 | let directoryURL: URL = url(filePath: path)
140 |
141 | try removeItem(
142 | at: directoryURL.appendingPathComponent(filename)
143 | )
144 | }
145 |
146 | func url(filePath: String) -> URL {
147 | #if !os(Linux) && !os(Windows)
148 | if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
149 | return URL(filePath: filePath)
150 | } else {
151 | return URL(fileURLWithPath: filePath)
152 | }
153 | #else
154 | return URL(fileURLWithPath: filePath)
155 | #endif
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/Helper/FileManaging.swift:
--------------------------------------------------------------------------------
1 | /// A protocol that provides methods for reading, writing, and deleting files in a type-safe, sendable manner.
2 | public protocol FileManaging: Sendable {
3 | /// Reads a file from the given path and decodes its contents into the specified type.
4 | /// - Parameters:
5 | /// - path: The directory path where the file is located. Defaults to the current directory `"."`.
6 | /// - filename: The name of the file to read.
7 | /// - Returns: The decoded value of the file's content as the specified type.
8 | /// - Throws: An error if the file cannot be found or decoded.
9 | func `in`(path: String, filename: String) throws -> Value
10 |
11 | /// Encodes and writes the given value to a file at the specified path.
12 | /// - Parameters:
13 | /// - value: The value to encode and write to the file. It must conform to `Encodable`.
14 | /// - path: The directory path where the file will be written. Defaults to the current directory `"."`.
15 | /// - filename: The name of the file to write.
16 | /// - base64Encoded: Whether to encode the content as Base64. Defaults to `true`.
17 | /// - Throws: An error if the file cannot be written.
18 | func `out`(_ value: Value, path: String, filename: String, base64Encoded: Bool) throws
19 |
20 | /// Deletes a file at the specified path.
21 | /// - Parameters:
22 | /// - path: The directory path where the file is located. Defaults to the current directory `"."`.
23 | /// - filename: The name of the file to delete.
24 | /// - Throws: An error if the file cannot be deleted.
25 | func `delete`(path: String, filename: String) throws
26 |
27 | /// Removes a file or directory at the specified path.
28 | /// - Parameter path: The full path of the file or directory to remove.
29 | /// - Throws: An error if the item cannot be removed.
30 | func removeItem(atPath path: String) throws
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/Helper/Loggable.swift:
--------------------------------------------------------------------------------
1 | protocol Loggable {
2 | @MainActor
3 | var logValue: String { get }
4 | }
5 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/Helper/MutableApplicationState.swift:
--------------------------------------------------------------------------------
1 | /**
2 | A protocol that represents a mutable application state.
3 |
4 | This protocol defines a type, `Value`, and a mutable property, `value`, of that type. It serves as the blueprint for any type that needs to represent a mutable state within an application.
5 | */
6 | public protocol MutableApplicationState {
7 | associatedtype Value
8 |
9 | /// An emoji to use when logging about this state.
10 | static var emoji: Character { get }
11 |
12 | /// The actual value that this state holds. It can be both retrieved and modified.
13 | @MainActor
14 | var value: Value { get set }
15 | }
16 |
17 | extension MutableApplicationState {
18 | static var emoji: Character { "❓" }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/Helper/UbiquitousKeyValueStoreManaging.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import Foundation
3 |
4 | /// A protocol that provides a thread-safe interface for interacting with `NSUbiquitousKeyValueStore`,
5 | /// which synchronizes key-value data across the user's iCloud-enabled devices.
6 | public protocol UbiquitousKeyValueStoreManaging: Sendable {
7 | /// Retrieves data stored in iCloud for the specified key.
8 | /// - Parameter key: The key used to retrieve the associated data from the `NSUbiquitousKeyValueStore`.
9 | /// - Returns: The `Data` object associated with the key, or `nil` if no data is found.
10 | func data(forKey key: String) -> Data?
11 |
12 | /// Sets a `Data` object for the specified key in iCloud's key-value store.
13 | /// - Parameters:
14 | /// - value: The `Data` object to store. Pass `nil` to remove the data associated with the key.
15 | /// - key: The key with which to associate the data.
16 | func set(_ value: Data?, forKey key: String)
17 |
18 | /// Removes the value associated with the specified key from iCloud's key-value store.
19 | /// - Parameter key: The key whose associated value should be removed.
20 | func removeObject(forKey key: String)
21 | }
22 | #endif
23 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/Helper/UserDefaultsManaging.swift:
--------------------------------------------------------------------------------
1 | /// A protocol that provides a thread-safe interface for interacting with `UserDefaults`,
2 | /// allowing the storage, retrieval, and removal of user preferences and data.
3 | public protocol UserDefaultsManaging: Sendable {
4 | /// Retrieves an object from `UserDefaults` for the given key.
5 | /// - Parameter key: The key used to retrieve the associated value from `UserDefaults`.
6 | /// - Returns: The value stored in `UserDefaults` for the given key, or `nil` if no value is associated with the key.
7 | func object(forKey key: String) -> Any?
8 |
9 | /// Removes the value associated with the specified key from `UserDefaults`.
10 | /// - Parameter key: The key whose associated value should be removed.
11 | func removeObject(forKey key: String)
12 |
13 | /// Sets the value for the specified key in `UserDefaults`.
14 | /// - Parameters:
15 | /// - value: The value to store in `UserDefaults`. Can be `nil` to remove the value associated with the key.
16 | /// - key: The key with which to associate the value.
17 | func set(_ value: Any?, forKey key: String)
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/State/Application+FileState.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Application {
4 | /// A struct that provides methods for reading, writing, and deleting files in a type-safe, sendable manner.
5 | public struct SendableFileManager: FileManaging {
6 | public func `in`(
7 | path: String = ".",
8 | filename: String
9 | ) throws -> Value {
10 | try FileManager.default.in(path: path, filename: filename)
11 | }
12 |
13 | public func `out`(
14 | _ value: Value,
15 | path: String = ".",
16 | filename: String,
17 | base64Encoded: Bool = true
18 | ) throws {
19 | try FileManager.default.out(
20 | value,
21 | path: path,
22 | filename: filename,
23 | base64Encoded: base64Encoded
24 | )
25 | }
26 |
27 | public func `delete`(path: String = ".", filename: String) throws {
28 | try FileManager.default.delete(path: path, filename: filename)
29 | }
30 |
31 | public func removeItem(atPath path: String) throws {
32 | try FileManager.default.removeItem(atPath: path)
33 | }
34 | }
35 |
36 | /// The shared `FileManager` instance.
37 | public var fileManager: Dependency {
38 | dependency(SendableFileManager())
39 | }
40 |
41 | /// `FileState` encapsulates the value within the application's scope and allows any changes to be propagated throughout the scoped area. State is stored using `FileManager`.
42 | public struct FileState: MutableApplicationState {
43 | public static var emoji: Character { "🗄️" }
44 |
45 | @AppDependency(\.fileManager) private var fileManager: FileManaging
46 |
47 | /// The initial value of the state.
48 | private var initial: () -> Value
49 |
50 | /// The current state value.
51 | public var value: Value {
52 | get {
53 | let cachedValue = shared.cache.get(
54 | scope.key,
55 | as: State.self
56 | )
57 |
58 | if let cachedValue = cachedValue {
59 | return cachedValue.value
60 | }
61 |
62 | do {
63 | let storedValue: Value = try fileManager.in(path: path, filename: filename)
64 |
65 | defer {
66 | let setValue = {
67 | shared.cache.set(
68 | value: Application.State(
69 | type: .file,
70 | initial: storedValue,
71 | scope: scope
72 | ),
73 | forKey: scope.key
74 | )
75 | }
76 |
77 | #if (!os(Linux) && !os(Windows))
78 | if NSClassFromString("XCTest") == nil {
79 | Task {
80 | await MainActor.run {
81 | setValue()
82 | }
83 | }
84 | } else {
85 | setValue()
86 | }
87 | #else
88 | setValue()
89 | #endif
90 | }
91 |
92 | return storedValue
93 | } catch {
94 | log(
95 | error: error,
96 | message: "\(FileState.emoji) FileState Fetching",
97 | fileID: #fileID,
98 | function: #function,
99 | line: #line,
100 | column: #column
101 | )
102 |
103 | return initial()
104 | }
105 | }
106 | set {
107 | let mirror = Mirror(reflecting: newValue)
108 |
109 | if mirror.displayStyle == .optional,
110 | mirror.children.isEmpty {
111 | shared.cache.remove(scope.key)
112 | do {
113 | try fileManager.delete(path: path, filename: filename)
114 | } catch {
115 | log(
116 | error: error,
117 | message: "\(FileState.emoji) FileState Deleting",
118 | fileID: #fileID,
119 | function: #function,
120 | line: #line,
121 | column: #column
122 | )
123 | }
124 | } else {
125 | shared.cache.set(
126 | value: Application.State(
127 | type: .file,
128 | initial: newValue,
129 | scope: scope
130 | ),
131 | forKey: scope.key
132 | )
133 |
134 | do {
135 | try fileManager.out(
136 | newValue,
137 | path: path,
138 | filename: filename,
139 | base64Encoded: isBase64Encoded
140 | )
141 | } catch {
142 | log(
143 | error: error,
144 | message: "\(FileState.emoji) FileState Saving",
145 | fileID: #fileID,
146 | function: #function,
147 | line: #line,
148 | column: #column
149 | )
150 | }
151 | }
152 | }
153 | }
154 |
155 | /// The scope in which this state exists.
156 | let scope: Scope
157 |
158 | let isBase64Encoded: Bool
159 |
160 | var path: String { scope.name }
161 | var filename: String { scope.id }
162 |
163 | /**
164 | Creates a new state within a given scope initialized with the provided value.
165 |
166 | - Parameters:
167 | - value: The initial value of the state
168 | - scope: The scope in which the state exists
169 | */
170 | init(
171 | initial: @escaping @autoclosure () -> Value,
172 | scope: Scope,
173 | isBase64Encoded: Bool
174 | ) {
175 | self.initial = initial
176 | self.scope = scope
177 | self.isBase64Encoded = isBase64Encoded
178 | }
179 |
180 | /// Resets the value to the inital value. If the inital value was `nil`, then the value will be removed from `FileManager`
181 | @MainActor
182 | public mutating func reset() {
183 | value = initial()
184 | }
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/State/Application+SecureState.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import Security
3 | import Foundation
4 |
5 | extension Application {
6 | /// The default `Keychain` instance.
7 | public var keychain: Dependency {
8 | dependency(Keychain())
9 | }
10 |
11 | /// The SecureState structure provides secure and persistent key-value string storage backed by the Keychain that can be used across the application.
12 | public struct SecureState: MutableApplicationState {
13 | public static var emoji: Character { "🔑" }
14 |
15 | @AppDependency(\.keychain) private var keychain: Keychain
16 |
17 | /// The initial value of the state.
18 | private var initial: () -> String?
19 |
20 | /// The current state value.
21 | /// Reading this value will return the stored string value in the keychain if it exists, otherwise, it will return the initial value.
22 | /// Writing a new string value to the state, it will be stored securely in the keychain. Writing `nil` will remove the corresponding key-value pair from the keychain store.
23 | public var value: String? {
24 | get {
25 | guard
26 | let storedValue = keychain.get(scope.key, as: String.self)
27 | else { return initial() }
28 |
29 | return storedValue
30 | }
31 | set {
32 | shared.objectWillChange.send()
33 |
34 | guard let newValue else {
35 | return keychain.remove(scope.key)
36 | }
37 |
38 | keychain.set(value: newValue, forKey: scope.key)
39 | }
40 | }
41 |
42 | /// The scope in which this state exists.
43 | let scope: Scope
44 |
45 | /**
46 | Creates a new state within a given scope initialized with the provided value.
47 |
48 | - Parameters:
49 | - value: The initial value of the state
50 | - scope: The scope in which the state exists
51 | */
52 | init(
53 | initial: @escaping @autoclosure () -> String?,
54 | scope: Scope
55 | ) {
56 | self.initial = initial
57 | self.scope = scope
58 | }
59 |
60 | /// Resets the state value to the initial value and store it in the keychain.
61 | @MainActor
62 | public mutating func reset() {
63 | value = initial()
64 | }
65 | }
66 | }
67 | #endif
68 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/State/Application+State.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Application {
4 | /// `State` encapsulates the value within the application's scope and allows any changes to be propagated throughout the scoped area.
5 | public struct State: Sendable, MutableApplicationState, Loggable {
6 | /// Values that are available in the cache.
7 | enum StateType {
8 | case state
9 | case stored
10 | case file
11 | }
12 |
13 | public static var emoji: Character {
14 | #if !os(Linux) && !os(Windows)
15 | return "🔄"
16 | #else
17 | return "📦"
18 | #endif
19 | }
20 |
21 | private let type: StateType
22 |
23 | /// A private backing storage for the value.
24 | private var _value: Value
25 |
26 | /// The current state value.
27 | @MainActor
28 | public var value: Value {
29 | get {
30 | guard
31 | let state = shared.cache.get(
32 | scope.key,
33 | as: State.self
34 | )
35 | else {
36 | defer {
37 | let setValue = {
38 | shared.cache.set(
39 | value: Application.State(
40 | type: .state,
41 | initial: _value,
42 | scope: scope
43 | ),
44 | forKey: scope.key
45 | )
46 | }
47 | #if (!os(Linux) && !os(Windows))
48 | if NSClassFromString("XCTest") == nil {
49 | Task { @MainActor in
50 | setValue()
51 | }
52 | } else {
53 | setValue()
54 | }
55 | #else
56 | setValue()
57 | #endif
58 | }
59 | return _value
60 | }
61 | return state._value
62 | }
63 | set {
64 | _value = newValue
65 | shared.cache.set(
66 | value: Application.State(
67 | type: .state,
68 | initial: newValue,
69 | scope: scope
70 | ),
71 | forKey: scope.key
72 | )
73 | }
74 | }
75 |
76 | /// The scope in which this state exists.
77 | let scope: Scope
78 |
79 | /**
80 | Creates a new state within a given scope initialized with the provided value.
81 |
82 | - Parameters:
83 | - value: The initial value of the state
84 | - scope: The scope in which the state exists
85 | */
86 | init(
87 | type: StateType,
88 | initial value: Value,
89 | scope: Scope
90 | ) {
91 | self.type = type
92 | self._value = value
93 | self.scope = scope
94 | }
95 |
96 | @MainActor
97 | public var logValue: String {
98 | switch type {
99 | case .state: return "State<\(Value.self)>(\(value)) (\(scope.key))"
100 | case .stored: return "StoredState<\(Value.self)>(\(value)) (\(scope.key))"
101 | case .file: return "FileState<\(Value.self)>(\(value)) (\(scope.key))"
102 | }
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/State/Application+StoredState.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Application {
4 | /// A struct that provides a thread-safe interface for interacting with `UserDefaults`, allowing the storage, retrieval, and removal of user preferences and data.
5 | public struct SendableUserDefaults: UserDefaultsManaging {
6 | public func object(forKey key: String) -> Any? {
7 | UserDefaults.standard.object(forKey: key)
8 | }
9 |
10 | public func removeObject(forKey key: String) {
11 | UserDefaults.standard.removeObject(forKey: key)
12 | }
13 |
14 | public func set(_ value: Any?, forKey key: String) {
15 | UserDefaults.standard.set(value, forKey: key)
16 | }
17 | }
18 |
19 | /// The shared `UserDefaults` instance.
20 | public var userDefaults: Dependency {
21 | dependency(SendableUserDefaults())
22 | }
23 |
24 | /// `StoredState` encapsulates the value within the application's scope and allows any changes to be propagated throughout the scoped area. State is stored using `UserDefaults`.
25 | public struct StoredState: MutableApplicationState {
26 | public static var emoji: Character { "💾" }
27 |
28 | @AppDependency(\.userDefaults) private var userDefaults: UserDefaultsManaging
29 |
30 | /// The initial value of the state.
31 | private var initial: () -> Value
32 |
33 | /// The current state value.
34 | public var value: Value {
35 | get {
36 | let cachedValue = shared.cache.get(
37 | scope.key,
38 | as: State.self
39 | )
40 |
41 | if let cachedValue = cachedValue {
42 | return cachedValue.value
43 | }
44 |
45 | guard
46 | let object = userDefaults.object(forKey: scope.key)
47 | else { return initial() }
48 |
49 | if
50 | let data = object as? Data,
51 | let decodedValue = try? JSONDecoder().decode(Value.self, from: data)
52 | {
53 | return decodedValue
54 | }
55 |
56 | guard
57 | let storedValue = object as? Value
58 | else { return initial() }
59 |
60 | return storedValue
61 | }
62 | set {
63 | let mirror = Mirror(reflecting: newValue)
64 |
65 | if mirror.displayStyle == .optional,
66 | mirror.children.isEmpty {
67 | shared.cache.remove(scope.key)
68 | userDefaults.removeObject(forKey: scope.key)
69 | } else {
70 | shared.cache.set(
71 | value: Application.State(
72 | type: .stored,
73 | initial: newValue,
74 | scope: scope
75 | ),
76 | forKey: scope.key
77 | )
78 |
79 | if let encodedValue = try? JSONEncoder().encode(newValue) {
80 | userDefaults.set(encodedValue, forKey: scope.key)
81 | } else {
82 | userDefaults.set(newValue, forKey: scope.key)
83 | }
84 | }
85 | }
86 | }
87 |
88 | /// The scope in which this state exists.
89 | let scope: Scope
90 |
91 | /**
92 | Creates a new state within a given scope initialized with the provided value.
93 |
94 | - Parameters:
95 | - value: The initial value of the state
96 | - scope: The scope in which the state exists
97 | */
98 | init(
99 | initial: @escaping @autoclosure () -> Value,
100 | scope: Scope
101 | ) {
102 | self.initial = initial
103 | self.scope = scope
104 | }
105 |
106 | /// Resets the value to the inital value. If the inital value was `nil`, then the value will be removed from `UserDefaults`
107 | @MainActor
108 | public mutating func reset() {
109 | value = initial()
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/State/Application+SyncState.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import Foundation
3 |
4 | @available(watchOS 9.0, *)
5 | extension Application {
6 | /// A struct that provides a thread-safe interface for interacting with `NSUbiquitousKeyValueStore`,
7 | /// which synchronizes small amounts of key-value data across the user's iCloud-enabled devices.
8 | public struct SendableNSUbiquitousKeyValueStore: UbiquitousKeyValueStoreManaging {
9 | public func data(forKey key: String) -> Data? {
10 | NSUbiquitousKeyValueStore.default.data(forKey: key)
11 | }
12 |
13 | public func set(_ value: Data?, forKey key: String) {
14 | NSUbiquitousKeyValueStore.default.set(value, forKey: key)
15 | }
16 |
17 | public func removeObject(forKey key: String) {
18 | NSUbiquitousKeyValueStore.default.removeObject(forKey: key)
19 | }
20 | }
21 |
22 | /// The default `NSUbiquitousKeyValueStore` instance.
23 | public var icloudStore: Dependency {
24 | dependency {
25 | NotificationCenter.default.addObserver(
26 | self,
27 | selector: #selector(didChangeExternally),
28 | name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
29 | object: NSUbiquitousKeyValueStore.default
30 | )
31 |
32 | return SendableNSUbiquitousKeyValueStore()
33 | }
34 | }
35 |
36 | /**
37 | The `SyncState` struct is a data structure designed to handle the state synchronization of an application that supports the `Codable` type.
38 | It utilizes Apple's iCloud Key-Value Store to propagate state changes across multiple devices.
39 |
40 | Changes your app writes to the key-value store object are initially held in memory, then written to disk by the system at appropriate times. If you write to the key-value store object when the user is not signed into an iCloud account, the data is stored locally until the next synchronization opportunity. When the user signs into an iCloud account, the system automatically reconciles your local, on-disk keys and values with those on the iCloud server.
41 |
42 | The total amount of space available in your app’s key-value store, for a given user, is 1 MB. There is a per-key value size limit of 1 MB, and a maximum of 1024 keys. If you attempt to write data that exceeds these quotas, the write attempt fails and no change is made to your iCloud key-value storage. In this scenario, the system posts the didChangeExternallyNotification notification with a change reason of NSUbiquitousKeyValueStoreQuotaViolationChange.
43 |
44 | - Note: The key-value store is intended for storing data that changes infrequently. As you test your devices, if the app on a device makes frequent changes to the key-value store, the system may defer the synchronization of some changes in order to minimize the number of round trips to the server. The more frequently the app make changes, the more likely the changes will be deferred and will not immediately show up on the other devices.
45 |
46 | The maximum length for key strings for the iCloud key-value store is 64 bytes using UTF8 encoding. Attempting to write a value to a longer key name results in a runtime error.
47 |
48 | To use this class, you must distribute your app through the App Store or Mac App Store, and you must request the com.apple.developer.ubiquity-kvstore-identifier entitlement in your Xcode project.
49 |
50 | - Warning: Avoid using this class for data that is essential to your app’s behavior when offline; instead, store such data directly into the local user defaults database.
51 | */
52 | public struct SyncState: MutableApplicationState {
53 | public static var emoji: Character { "☁️" }
54 |
55 | @AppDependency(\.icloudStore) private var icloudStore: UbiquitousKeyValueStoreManaging
56 |
57 | /// The initial value of the state.
58 | private var initial: () -> Value
59 |
60 | /// The scope in which this state exists.
61 | private let scope: Scope
62 |
63 | /// The fallback state for when icloudStore doesn't have the data.
64 | private var storedState: StoredState
65 |
66 | /// The current state value.
67 | /// This value is retrieved from the iCloud Key-Value Store or the local cache.
68 | /// If the value is not found in either, the initial value is returned.
69 | /// When setting a new value, the value is saved to the iCloud Key-Value Store and the local cache.
70 | public var value: Value {
71 | get {
72 | if
73 | let data = icloudStore.data(forKey: scope.key),
74 | let storedValue = try? JSONDecoder().decode(Value.self, from: data)
75 | {
76 | return storedValue
77 | }
78 |
79 | return storedState.value
80 | }
81 | set {
82 | let mirror = Mirror(reflecting: newValue)
83 |
84 | if mirror.displayStyle == .optional,
85 | mirror.children.isEmpty {
86 | storedState.reset()
87 | icloudStore.removeObject(forKey: scope.key)
88 | } else {
89 | storedState.value = newValue
90 |
91 | do {
92 | let data = try JSONEncoder().encode(newValue)
93 | icloudStore.set(data, forKey: scope.key)
94 | } catch {
95 | Application.log(
96 | error: error,
97 | message: "\(SyncState.emoji) SyncState failed to encode: \(newValue)",
98 | fileID: #fileID,
99 | function: #function,
100 | line: #line,
101 | column: #column
102 | )
103 | }
104 | }
105 | }
106 | }
107 |
108 |
109 | /**
110 | Creates a new state within a given scope initialized with the provided value.
111 |
112 | - Parameters:
113 | - value: The initial value of the state
114 | - scope: The scope in which the state exists
115 | */
116 | init(
117 | initial: @escaping @autoclosure () -> Value,
118 | scope: Scope
119 | ) {
120 | self.initial = initial
121 | self.scope = scope
122 | self.storedState = StoredState(initial: initial(), scope: scope)
123 | }
124 |
125 | /// Resets the value to the inital value. If the inital value was `nil`, then the value will be removed from `iCloud`
126 | @MainActor
127 | public mutating func reset() {
128 | value = initial()
129 | }
130 | }
131 | }
132 | #endif
133 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/State/Slice/Application+OptionalSlice.swift:
--------------------------------------------------------------------------------
1 | extension Application {
2 | /// `OptionalSlice` allows access and modification to a specific part of an AppState's state. Supports `State`, `SyncState`, and `StorageState`.
3 | public struct OptionalSlice<
4 | SlicedState: MutableApplicationState,
5 | Value,
6 | SliceValue,
7 | SliceKeyPath: KeyPath
8 | > where SlicedState.Value == Value? {
9 | /// A private backing storage for the value.
10 | private var state: SlicedState
11 | private let keyPath: SliceKeyPath
12 |
13 | @MainActor
14 | init(
15 | _ stateKeyPath: KeyPath,
16 | value valueKeyPath: SliceKeyPath
17 | ) {
18 | self.state = shared.value(keyPath: stateKeyPath)
19 | self.keyPath = valueKeyPath
20 | }
21 | }
22 | }
23 |
24 | extension Application.OptionalSlice where SliceKeyPath == KeyPath {
25 | /// The current state value.
26 | @MainActor
27 | public var value: SliceValue? {
28 | state.value?[keyPath: keyPath]
29 | }
30 | }
31 |
32 | extension Application.OptionalSlice where SliceKeyPath == WritableKeyPath {
33 | /// The current state value.
34 | @MainActor
35 | public var value: SliceValue? {
36 | get {
37 | guard let sliceState = state.value else { return nil }
38 |
39 | return sliceState[keyPath: keyPath]
40 | }
41 | set {
42 | guard var sliceState = state.value else { return }
43 |
44 | if let newValue {
45 | sliceState[keyPath: keyPath] = newValue
46 | }
47 |
48 | state.value = sliceState
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/State/Slice/Application+OptionalSliceOptionalValue.swift:
--------------------------------------------------------------------------------
1 | extension Application {
2 | /// `OptionalSliceOptionalValue` allows access and modification to a specific part of an AppState's state. Supports `State`, `SyncState`, and `StorageState`.
3 | public struct OptionalSliceOptionalValue<
4 | SlicedState: MutableApplicationState,
5 | Value,
6 | SliceValue,
7 | SliceKeyPath: KeyPath
8 | > where SlicedState.Value == Value? {
9 | /// A private backing storage for the value.
10 | private var state: SlicedState
11 | private let keyPath: SliceKeyPath
12 |
13 | @MainActor
14 | init(
15 | _ stateKeyPath: KeyPath,
16 | value valueKeyPath: SliceKeyPath
17 | ) {
18 | self.state = shared.value(keyPath: stateKeyPath)
19 | self.keyPath = valueKeyPath
20 | }
21 | }
22 | }
23 |
24 | extension Application.OptionalSliceOptionalValue where SliceKeyPath == KeyPath {
25 | /// The current state value.
26 | @MainActor
27 | public var value: SliceValue? {
28 | state.value?[keyPath: keyPath]
29 | }
30 | }
31 |
32 | extension Application.OptionalSliceOptionalValue where SliceKeyPath == WritableKeyPath {
33 | /// The current state value.
34 | @MainActor
35 | public var value: SliceValue? {
36 | get {
37 | guard let sliceState = state.value else { return nil }
38 |
39 | return sliceState[keyPath: keyPath]
40 | }
41 | set {
42 | guard var sliceState = state.value else { return }
43 |
44 | sliceState[keyPath: keyPath] = newValue
45 |
46 | state.value = sliceState
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/AppState/Application/Types/State/Slice/Application+Slice.swift:
--------------------------------------------------------------------------------
1 | extension Application {
2 | /// `Slice` allows access and modification to a specific part of an AppState's state. Supports `State`, `SyncState`, and `StorageState`.
3 | public struct Slice<
4 | SlicedState: MutableApplicationState,
5 | Value,
6 | SliceValue,
7 | SliceKeyPath: KeyPath
8 | > where SlicedState.Value == Value {
9 | /// A private backing storage for the value.
10 | private var state: SlicedState
11 | private let keyPath: SliceKeyPath
12 |
13 | @MainActor
14 | init(
15 | _ stateKeyPath: KeyPath,
16 | value valueKeyPath: SliceKeyPath
17 | ) {
18 | self.state = shared.value(keyPath: stateKeyPath)
19 | self.keyPath = valueKeyPath
20 | }
21 | }
22 | }
23 |
24 | extension Application.Slice where SliceKeyPath == KeyPath {
25 | /// The current state value.
26 | @MainActor
27 | public var value: SliceValue {
28 | state.value[keyPath: keyPath]
29 | }
30 | }
31 |
32 | extension Application.Slice where SliceKeyPath == WritableKeyPath {
33 | /// The current state value.
34 | @MainActor
35 | public var value: SliceValue {
36 | get { state.value[keyPath: keyPath] }
37 | set { state.value[keyPath: keyPath] = newValue }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/AppState/PropertyWrappers/Dependency/AppDependency.swift:
--------------------------------------------------------------------------------
1 | /// The `@AppDependency` property wrapper is a feature provided by AppState, intended to simplify dependency handling throughout your application. It makes it easy to access, share, and manage dependencies in a neat and Swift idiomatic way.
2 | @propertyWrapper public struct AppDependency {
3 | /// Path for accessing `Dependency` from Application.
4 | private let keyPath: KeyPath>
5 |
6 | private let fileID: StaticString
7 | private let function: StaticString
8 | private let line: Int
9 | private let column: Int
10 |
11 | /// Represents the current value of the `Dependency`.
12 | @MainActor
13 | public var wrappedValue: Value {
14 | Application.dependency(
15 | keyPath,
16 | fileID,
17 | function,
18 | line,
19 | column
20 | )
21 | }
22 |
23 | /**
24 | Initializes the AppDependency with a `keyPath` for accessing `Dependency` in Application.
25 |
26 | - Parameter keyPath: The `KeyPath` for accessing `Dependency` in Application.
27 | */
28 | public init(
29 | _ keyPath: KeyPath>,
30 | _ fileID: StaticString = #fileID,
31 | _ function: StaticString = #function,
32 | _ line: Int = #line,
33 | _ column: Int = #column
34 | ) {
35 | self.keyPath = keyPath
36 | self.fileID = fileID
37 | self.function = function
38 | self.line = line
39 | self.column = column
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/AppState/PropertyWrappers/Dependency/ObservedDependency.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import SwiftUI
3 |
4 | /// The `@ObservedDependency` property wrapper is a feature provided by AppState, intended to simplify dependency handling throughout your application. It makes it easy to access, share, and manage dependencies in a neat and Swift idiomatic way. It works the same as `@AppDependency`, but comes with the power of the `@ObservedObject` property wrapper.
5 | @MainActor
6 | @propertyWrapper public struct ObservedDependency: DynamicProperty where Value: ObservableObject {
7 | /// Path for accessing `ObservedDependency` from Application.
8 | private let keyPath: KeyPath>
9 |
10 | @ObservedObject private var observedObject: Value
11 |
12 | private let fileID: StaticString
13 | private let function: StaticString
14 | private let line: Int
15 | private let column: Int
16 |
17 | /// Represents the current value of the `ObservedDependency`.
18 | @MainActor
19 | public var wrappedValue: Value {
20 | Application.log(
21 | debug: "🔗 Getting ObservedDependency \(String(describing: keyPath))",
22 | fileID: fileID,
23 | function: function,
24 | line: line,
25 | column: column
26 | )
27 |
28 | return observedObject
29 | }
30 |
31 | /// A binding to the `ObservedDependency`'s value, which can be used with SwiftUI views.
32 | @MainActor
33 | public var projectedValue: ObservedObject.Wrapper {
34 | Application.log(
35 | debug: "🔗 Getting ObservedDependency \(String(describing: keyPath))",
36 | fileID: fileID,
37 | function: function,
38 | line: line,
39 | column: column
40 | )
41 |
42 | return $observedObject
43 | }
44 |
45 | /**
46 | Initializes the ObservedDependency.
47 |
48 | - Parameter keyPath: The `KeyPath` for accessing `Dependency` in Application.
49 | */
50 | @MainActor
51 | public init(
52 | _ keyPath: KeyPath>,
53 | _ fileID: StaticString = #fileID,
54 | _ function: StaticString = #function,
55 | _ line: Int = #line,
56 | _ column: Int = #column
57 | ) {
58 | self.keyPath = keyPath
59 | self.fileID = fileID
60 | self.function = function
61 | self.line = line
62 | self.column = column
63 |
64 | self.observedObject = Application.dependency(
65 | keyPath,
66 | fileID,
67 | function,
68 | line,
69 | column
70 | )
71 | }
72 | }
73 | #endif
74 |
--------------------------------------------------------------------------------
/Sources/AppState/PropertyWrappers/Dependency/Slice/DependencyConstant.swift:
--------------------------------------------------------------------------------
1 | /// A property wrapper that provides access to a specific part of the AppState's dependencies.
2 | @propertyWrapper public struct DependencyConstant> {
3 | /// Path for accessing `Dependency` from Application.
4 | private let dependencyKeyPath: KeyPath>
5 |
6 | /// Path for accessing `SliceValue` from `Value`.
7 | private let valueKeyPath: SliceKeyPath
8 |
9 | private let fileID: StaticString
10 | private let function: StaticString
11 | private let line: Int
12 | private let column: Int
13 | private let sliceKeyPath: String
14 |
15 | /// Represents the current value of the `Dependency`.
16 | @MainActor
17 | public var wrappedValue: SliceValue {
18 | Application.dependencySlice(
19 | dependencyKeyPath,
20 | valueKeyPath,
21 | fileID,
22 | function,
23 | line,
24 | column
25 | ).value
26 | }
27 |
28 | /**
29 | Initializes a Constant with the provided parameters. This constructor is used to create a Constant that provides access and modification to a specific part of an AppState's dependencies. It provides granular control over the AppState.
30 |
31 | - Parameters:
32 | - dependencyKeyPath: A KeyPath that points to the dependency in AppState that should be sliced.
33 | - valueKeyPath: A KeyPath that points to the specific part of the dependency that should be accessed.
34 | */
35 | @MainActor
36 | public init(
37 | _ dependencyKeyPath: KeyPath>,
38 | _ valueKeyPath: KeyPath,
39 | _ fileID: StaticString = #fileID,
40 | _ function: StaticString = #function,
41 | _ line: Int = #line,
42 | _ column: Int = #column
43 | ) where SliceKeyPath == KeyPath {
44 | self.dependencyKeyPath = dependencyKeyPath
45 | self.valueKeyPath = valueKeyPath
46 | self.fileID = fileID
47 | self.function = function
48 | self.line = line
49 | self.column = column
50 |
51 | let dependencyKeyPathString = String(describing: dependencyKeyPath)
52 | let valueTypeCharacterCount = String(describing: Value.self).count
53 | var valueKeyPathString = String(describing: valueKeyPath)
54 |
55 | valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)
56 |
57 | self.sliceKeyPath = "\(dependencyKeyPathString)\(valueKeyPathString)"
58 | }
59 |
60 | /**
61 | Initializes a Constant with the provided parameters. This constructor is used to create a Constant that provides access and modification to a specific part of an AppState's dependencies. It provides granular control over the AppState.
62 |
63 | - Parameters:
64 | - dependencyKeyPath: A KeyPath that points to the dependency in AppState that should be sliced.
65 | - valueKeyPath: A WritableKeyPath that points to the specific part of the state that should be accessed.
66 | */
67 | @MainActor
68 | public init(
69 | _ dependencyKeyPath: KeyPath>,
70 | _ valueKeyPath: WritableKeyPath,
71 | _ fileID: StaticString = #fileID,
72 | _ function: StaticString = #function,
73 | _ line: Int = #line,
74 | _ column: Int = #column
75 | ) where SliceKeyPath == WritableKeyPath {
76 | self.dependencyKeyPath = dependencyKeyPath
77 | self.valueKeyPath = valueKeyPath
78 | self.fileID = fileID
79 | self.function = function
80 | self.line = line
81 | self.column = column
82 |
83 | let dependencyKeyPathString = String(describing: dependencyKeyPath)
84 | let valueTypeCharacterCount = String(describing: Value.self).count
85 | var valueKeyPathString = String(describing: valueKeyPath)
86 |
87 | valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)
88 |
89 | self.sliceKeyPath = "\(dependencyKeyPathString)\(valueKeyPathString)"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Sources/AppState/PropertyWrappers/Dependency/Slice/DependencySlice.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import Combine
3 | import SwiftUI
4 | #endif
5 |
6 | /// A property wrapper that provides access to a specific part of the AppState's dependencies.
7 | @propertyWrapper public struct DependencySlice {
8 | #if !os(Linux) && !os(Windows)
9 | /// Holds the singleton instance of `Application`.
10 | @ObservedObject private var app: Application = Application.shared
11 | #else
12 | /// Holds the singleton instance of `Application`.
13 | @MainActor
14 | private var app: Application = Application.shared
15 | #endif
16 |
17 | /// Path for accessing `Dependency` from Application.
18 | private let dependencyKeyPath: KeyPath>
19 |
20 | /// Path for accessing `SliceValue` from `Value`.
21 | private let valueKeyPath: WritableKeyPath
22 |
23 | private let fileID: StaticString
24 | private let function: StaticString
25 | private let line: Int
26 | private let column: Int
27 | private let sliceKeyPath: String
28 |
29 | /// Represents the current value of the `Dependency`.
30 | @MainActor
31 | public var wrappedValue: SliceValue {
32 | get {
33 | Application.dependencySlice(
34 | dependencyKeyPath,
35 | valueKeyPath,
36 | fileID,
37 | function,
38 | line,
39 | column
40 | ).value
41 | }
42 | nonmutating set {
43 | Application.log(
44 | debug: "🔗 Setting DependencySlice \(sliceKeyPath) = \(newValue)",
45 | fileID: fileID,
46 | function: function,
47 | line: line,
48 | column: column
49 | )
50 |
51 | var dependency = app.value(keyPath: dependencyKeyPath)
52 | #if !os(Linux) && !os(Windows)
53 | Application.shared.objectWillChange.send()
54 | #endif
55 | dependency.value[keyPath: valueKeyPath] = newValue
56 | }
57 | }
58 |
59 | #if !os(Linux) && !os(Windows)
60 | /// A binding to the `Dependency`'s value, which can be used with SwiftUI views.
61 | @MainActor
62 | public var projectedValue: Binding {
63 | Binding(
64 | get: { wrappedValue },
65 | set: { wrappedValue = $0 }
66 | )
67 | }
68 | #endif
69 |
70 | /**
71 | Initializes a DependencySlice with the provided parameters. This constructor is used to create a DependencySlice that provides access and modification to a specific part of an AppState's dependencies. It provides granular control over the AppState.
72 |
73 | - Parameters:
74 | - dependencyKeyPath: A KeyPath that points to the dependency in AppState that should be sliced.
75 | - valueKeyPath: A WritableKeyPath that points to the specific part of the state that should be accessed.
76 | */
77 | @MainActor
78 | public init(
79 | _ dependencyKeyPath: KeyPath>,
80 | _ valueKeyPath: WritableKeyPath,
81 | _ fileID: StaticString = #fileID,
82 | _ function: StaticString = #function,
83 | _ line: Int = #line,
84 | _ column: Int = #column
85 | ) {
86 | self.dependencyKeyPath = dependencyKeyPath
87 | self.valueKeyPath = valueKeyPath
88 | self.fileID = fileID
89 | self.function = function
90 | self.line = line
91 | self.column = column
92 |
93 | let dependencyKeyPathString = String(describing: dependencyKeyPath)
94 | let valueTypeCharacterCount = String(describing: Value.self).count
95 | var valueKeyPathString = String(describing: valueKeyPath)
96 |
97 | valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)
98 |
99 | self.sliceKeyPath = "\(dependencyKeyPathString)\(valueKeyPathString)"
100 | }
101 | }
102 |
103 | #if !os(Linux) && !os(Windows)
104 | extension DependencySlice: DynamicProperty { }
105 | #endif
106 |
--------------------------------------------------------------------------------
/Sources/AppState/PropertyWrappers/State/AppState.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import Combine
3 | import SwiftUI
4 | #endif
5 |
6 | /// `AppState` is a property wrapper allowing SwiftUI views to subscribe to Application's state changes in a reactive way. Works similar to `State` and `Published`.
7 | @propertyWrapper public struct AppState where ApplicationState.Value == Value {
8 | #if !os(Linux) && !os(Windows)
9 | /// Holds the singleton instance of `Application`.
10 | @ObservedObject private var app: Application = Application.shared
11 | #else
12 | /// Holds the singleton instance of `Application`.
13 | @MainActor
14 | private var app: Application = Application.shared
15 | #endif
16 |
17 | /// Path for accessing `State` from Application.
18 | private let keyPath: KeyPath
19 |
20 | private let fileID: StaticString
21 | private let function: StaticString
22 | private let line: Int
23 | private let column: Int
24 |
25 | /// Represents the current value of the `State`.
26 | @MainActor
27 | public var wrappedValue: Value {
28 | get {
29 | Application.state(
30 | keyPath,
31 | fileID,
32 | function,
33 | line,
34 | column
35 | ).value
36 | }
37 | nonmutating set {
38 | Application.log(
39 | debug: "\(ApplicationState.emoji) Setting State \(String(describing: keyPath)) = \(newValue)",
40 | fileID: fileID,
41 | function: function,
42 | line: line,
43 | column: column
44 | )
45 |
46 | var state = app.value(keyPath: keyPath)
47 | state.value = newValue
48 | }
49 | }
50 |
51 | #if !os(Linux) && !os(Windows)
52 | /// A binding to the `State`'s value, which can be used with SwiftUI views.
53 | @MainActor
54 | public var projectedValue: Binding {
55 | Binding(
56 | get: { wrappedValue },
57 | set: { wrappedValue = $0 }
58 | )
59 | }
60 | #endif
61 |
62 | /**
63 | Initializes the AppState with a `keyPath` for accessing `State` in Application.
64 |
65 | - Parameter keyPath: The `KeyPath` for accessing `State` in Application.
66 | */
67 | @MainActor
68 | public init(
69 | _ keyPath: KeyPath,
70 | _ fileID: StaticString = #fileID,
71 | _ function: StaticString = #function,
72 | _ line: Int = #line,
73 | _ column: Int = #column
74 | ) {
75 | self.keyPath = keyPath
76 | self.fileID = fileID
77 | self.function = function
78 | self.line = line
79 | self.column = column
80 | }
81 |
82 | #if !os(Linux) && !os(Windows)
83 | /// A property wrapper's synthetic storage property. This is just for SwiftUI to mutate the `wrappedValue` and send event through `objectWillChange` publisher when the `wrappedValue` changes
84 | @MainActor
85 | public static subscript(
86 | _enclosingInstance observed: OuterSelf,
87 | wrapped wrappedKeyPath: ReferenceWritableKeyPath,
88 | storage storageKeyPath: ReferenceWritableKeyPath
89 | ) -> Value {
90 | get {
91 | observed[keyPath: storageKeyPath].wrappedValue
92 | }
93 | set {
94 | guard
95 | let publisher = observed.objectWillChange as? ObservableObjectPublisher
96 | else { return }
97 |
98 | publisher.send()
99 | observed[keyPath: storageKeyPath].wrappedValue = newValue
100 | }
101 | }
102 | #endif
103 | }
104 |
105 | #if !os(Linux) && !os(Windows)
106 | extension AppState: DynamicProperty { }
107 | #endif
108 |
--------------------------------------------------------------------------------
/Sources/AppState/PropertyWrappers/State/FileState.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if !os(Linux) && !os(Windows)
3 | import Combine
4 | import SwiftUI
5 | #endif
6 |
7 | /// `FileState` is a property wrapper allowing SwiftUI views to subscribe to Application's state changes in a reactive way. State is stored using `FileManager`. Works similar to `State` and `Published`.
8 | @propertyWrapper public struct FileState {
9 | #if !os(Linux) && !os(Windows)
10 | /// Holds the singleton instance of `Application`.
11 | @ObservedObject private var app: Application = Application.shared
12 | #else
13 | /// Holds the singleton instance of `Application`.
14 | @MainActor
15 | private var app: Application = Application.shared
16 | #endif
17 |
18 | /// Path for accessing `FileState` from Application.
19 | private let keyPath: KeyPath>
20 |
21 | private let fileID: StaticString
22 | private let function: StaticString
23 | private let line: Int
24 | private let column: Int
25 |
26 | /// Represents the current value of the `FileState`.
27 | @MainActor
28 | public var wrappedValue: Value {
29 | get {
30 | Application.fileState(
31 | keyPath,
32 | fileID,
33 | function,
34 | line,
35 | column
36 | ).value
37 | }
38 | nonmutating set {
39 | Application.log(
40 | debug: "🗄️ Setting FileState \(String(describing: keyPath)) = \(newValue)",
41 | fileID: fileID,
42 | function: function,
43 | line: line,
44 | column: column
45 | )
46 |
47 | var state = app.value(keyPath: keyPath)
48 | state.value = newValue
49 | }
50 | }
51 |
52 | #if !os(Linux) && !os(Windows)
53 | /// A binding to the `State`'s value, which can be used with SwiftUI views.
54 | @MainActor
55 | public var projectedValue: Binding {
56 | Binding(
57 | get: { wrappedValue },
58 | set: { wrappedValue = $0 }
59 | )
60 | }
61 | #endif
62 |
63 | /**
64 | Initializes the AppState with a `keyPath` for accessing `FileState` in Application.
65 |
66 | - Parameter keyPath: The `KeyPath` for accessing `FileState` in Application.
67 | */
68 | @MainActor
69 | public init(
70 | _ keyPath: KeyPath>,
71 | _ fileID: StaticString = #fileID,
72 | _ function: StaticString = #function,
73 | _ line: Int = #line,
74 | _ column: Int = #column
75 | ) {
76 | self.keyPath = keyPath
77 | self.fileID = fileID
78 | self.function = function
79 | self.line = line
80 | self.column = column
81 | }
82 |
83 | #if !os(Linux) && !os(Windows)
84 | /// A property wrapper's synthetic storage property. This is just for SwiftUI to mutate the `wrappedValue` and send event through `objectWillChange` publisher when the `wrappedValue` changes
85 | @MainActor
86 | public static subscript(
87 | _enclosingInstance observed: OuterSelf,
88 | wrapped wrappedKeyPath: ReferenceWritableKeyPath,
89 | storage storageKeyPath: ReferenceWritableKeyPath
90 | ) -> Value {
91 | get {
92 | observed[keyPath: storageKeyPath].wrappedValue
93 | }
94 | set {
95 | guard
96 | let publisher = observed.objectWillChange as? ObservableObjectPublisher
97 | else { return }
98 |
99 | publisher.send()
100 | observed[keyPath: storageKeyPath].wrappedValue = newValue
101 | }
102 | }
103 | #endif
104 | }
105 |
106 | #if !os(Linux) && !os(Windows)
107 | extension FileState: DynamicProperty { }
108 | #endif
109 |
--------------------------------------------------------------------------------
/Sources/AppState/PropertyWrappers/State/SecureState.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import Combine
3 | import SwiftUI
4 |
5 | /**
6 | A property wrapper struct that represents secure and persistent storage for a wrapped value.
7 |
8 | The value is kept in the `Application`'s secure state and managed by SwiftUI's property wrapper mechanism.
9 | As a `DynamicProperty`, SwiftUI will update the owning view whenever the value changes.
10 | */
11 | @propertyWrapper public struct SecureState: DynamicProperty {
12 | /// Holds the singleton instance of `Application`.
13 | @ObservedObject private var app: Application = Application.shared
14 |
15 | /// Path for accessing `SecureState` from Application.
16 | private let keyPath: KeyPath
17 |
18 | private let fileID: StaticString
19 | private let function: StaticString
20 | private let line: Int
21 | private let column: Int
22 |
23 | /**
24 | The current value of the secure state.
25 |
26 | Reading this property returns the current value.
27 | Writing to the property will update the underlying value in the secure state and log the operation.
28 | */
29 | @MainActor
30 | public var wrappedValue: String? {
31 | get {
32 | Application.secureState(
33 | keyPath,
34 | fileID,
35 | function,
36 | line,
37 | column
38 | ).value
39 | }
40 | nonmutating set {
41 | Application.log(
42 | debug: {
43 | #if DEBUG
44 | "🔑 Setting SecureState \(String(describing: keyPath)) = \(newValue ?? "nil")"
45 | #else
46 | "🔑 Setting SecureState \(String(describing: keyPath))"
47 | #endif
48 | },
49 | fileID: fileID,
50 | function: function,
51 | line: line,
52 | column: column
53 | )
54 |
55 | var state = app.value(keyPath: keyPath)
56 | state.value = newValue
57 | }
58 | }
59 |
60 | /// A binding to the `State`'s value, which can be used with SwiftUI views.
61 | @MainActor
62 | public var projectedValue: Binding {
63 | Binding(
64 | get: { wrappedValue },
65 | set: { wrappedValue = $0 }
66 | )
67 | }
68 |
69 | /**
70 | Initializes the AppState with a `keyPath` for accessing `SecureState` in Application.
71 |
72 | - Parameter keyPath: The `KeyPath` for accessing `SecureState` in Application.
73 | */
74 | @MainActor
75 | public init(
76 | _ keyPath: KeyPath,
77 | _ fileID: StaticString = #fileID,
78 | _ function: StaticString = #function,
79 | _ line: Int = #line,
80 | _ column: Int = #column
81 | ) {
82 | self.keyPath = keyPath
83 | self.fileID = fileID
84 | self.function = function
85 | self.line = line
86 | self.column = column
87 | }
88 |
89 | /// A property wrapper's synthetic storage property. This is just for SwiftUI to mutate the `wrappedValue` and send event through `objectWillChange` publisher when the `wrappedValue` changes
90 | @MainActor
91 | public static subscript(
92 | _enclosingInstance observed: OuterSelf,
93 | wrapped wrappedKeyPath: ReferenceWritableKeyPath,
94 | storage storageKeyPath: ReferenceWritableKeyPath
95 | ) -> String? {
96 | get {
97 | observed[keyPath: storageKeyPath].wrappedValue
98 | }
99 | set {
100 | guard
101 | let publisher = observed.objectWillChange as? ObservableObjectPublisher
102 | else { return }
103 |
104 | publisher.send()
105 | observed[keyPath: storageKeyPath].wrappedValue = newValue
106 | }
107 | }
108 | }
109 | #endif
110 |
--------------------------------------------------------------------------------
/Sources/AppState/PropertyWrappers/State/Slice/Constant.swift:
--------------------------------------------------------------------------------
1 | /// A property wrapper that provides access to a specific part of the AppState's state.
2 | @propertyWrapper public struct Constant> where SlicedState.Value == Value {
3 | /// Path for accessing `State` from Application.
4 | private let stateKeyPath: KeyPath
5 |
6 | /// Path for accessing `SliceValue` from `Value`.
7 | private let valueKeyPath: SliceKeyPath
8 |
9 | private let fileID: StaticString
10 | private let function: StaticString
11 | private let line: Int
12 | private let column: Int
13 | private let sliceKeyPath: String
14 |
15 | /// Represents the current value of the `State`.
16 | @MainActor
17 | public var wrappedValue: SliceValue {
18 | Application.slice(
19 | stateKeyPath,
20 | valueKeyPath,
21 | fileID,
22 | function,
23 | line,
24 | column
25 | ).value
26 | }
27 |
28 | /**
29 | Initializes a Constant with the provided parameters. This constructor is used to create a Constant that provides access to a specific part of an AppState's state. It provides granular control over the AppState.
30 |
31 | - Parameters:
32 | - stateKeyPath: A KeyPath that points to the state in AppState that should be sliced.
33 | - valueKeyPath: A KeyPath that points to the specific part of the state that should be accessed.
34 | */
35 | @MainActor
36 | public init(
37 | _ stateKeyPath: KeyPath,
38 | _ valueKeyPath: KeyPath,
39 | _ fileID: StaticString = #fileID,
40 | _ function: StaticString = #function,
41 | _ line: Int = #line,
42 | _ column: Int = #column
43 | ) where SliceKeyPath == KeyPath {
44 | self.stateKeyPath = stateKeyPath
45 | self.valueKeyPath = valueKeyPath
46 | self.fileID = fileID
47 | self.function = function
48 | self.line = line
49 | self.column = column
50 |
51 | let stateKeyPathString = String(describing: stateKeyPath)
52 | let valueTypeCharacterCount = String(describing: Value.self).count
53 | var valueKeyPathString = String(describing: valueKeyPath)
54 |
55 | valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)
56 |
57 | self.sliceKeyPath = "\(stateKeyPathString)\(valueKeyPathString)"
58 | }
59 |
60 | /**
61 | Initializes a Constant with the provided parameters. This constructor is used to create a Constant that provides access to a specific part of an AppState's state. It provides granular control over the AppState.
62 |
63 | - Parameters:
64 | - stateKeyPath: A KeyPath that points to the state in AppState that should be sliced.
65 | - valueKeyPath: A WritableKeyPath that points to the specific part of the state that should be accessed.
66 | */
67 | @MainActor
68 | public init(
69 | _ stateKeyPath: KeyPath,
70 | _ valueKeyPath: WritableKeyPath,
71 | _ fileID: StaticString = #fileID,
72 | _ function: StaticString = #function,
73 | _ line: Int = #line,
74 | _ column: Int = #column
75 | ) where SliceKeyPath == WritableKeyPath {
76 | self.stateKeyPath = stateKeyPath
77 | self.valueKeyPath = valueKeyPath
78 | self.fileID = fileID
79 | self.function = function
80 | self.line = line
81 | self.column = column
82 |
83 | let stateKeyPathString = String(describing: stateKeyPath)
84 | let valueTypeCharacterCount = String(describing: Value.self).count
85 | var valueKeyPathString = String(describing: valueKeyPath)
86 |
87 | valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)
88 |
89 | self.sliceKeyPath = "\(stateKeyPathString)\(valueKeyPathString)"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Sources/AppState/PropertyWrappers/State/Slice/OptionalConstant.swift:
--------------------------------------------------------------------------------
1 | /// A property wrapper that provides access to a specific part of the AppState's state.
2 | @propertyWrapper public struct OptionalConstant where SlicedState.Value == Value? {
3 | /// Path for accessing `State` from Application.
4 | private let stateKeyPath: KeyPath
5 |
6 | /// Path for accessing `SliceValue` from `Value`.
7 | private let valueKeyPath: KeyPath?
8 |
9 | /// Path for accessing `SliceValue?` from `Value`.
10 | private let optionalValueKeyPath: KeyPath?
11 |
12 | private let fileID: StaticString
13 | private let function: StaticString
14 | private let line: Int
15 | private let column: Int
16 | private let sliceKeyPath: String
17 |
18 | /// Represents the current value of the `State`.
19 | @MainActor
20 | public var wrappedValue: SliceValue? {
21 | if let valueKeyPath {
22 | return Application.slice(
23 | stateKeyPath,
24 | valueKeyPath,
25 | fileID,
26 | function,
27 | line,
28 | column
29 | ).value
30 | }
31 |
32 | guard
33 | let optionalValueKeyPath,
34 | let slicedValue = Application.slice(
35 | stateKeyPath,
36 | optionalValueKeyPath,
37 | fileID,
38 | function,
39 | line,
40 | column
41 | ).value
42 | else {
43 | return nil
44 | }
45 |
46 | return slicedValue
47 | }
48 |
49 | /**
50 | Initializes a OptionalConstant with the provided parameters. This constructor is used to create a OptionalConstant that provides access to a specific part of an AppState's state. It provides granular control over the AppState.
51 |
52 | - Parameters:
53 | - stateKeyPath: A KeyPath that points to the state in AppState that should be sliced.
54 | - valueKeyPath: A KeyPath that points to the specific part of the state that should be accessed.
55 | */
56 | @MainActor
57 | public init(
58 | _ stateKeyPath: KeyPath,
59 | _ valueKeyPath: KeyPath,
60 | _ fileID: StaticString = #fileID,
61 | _ function: StaticString = #function,
62 | _ line: Int = #line,
63 | _ column: Int = #column
64 | ) {
65 | self.stateKeyPath = stateKeyPath
66 | self.valueKeyPath = valueKeyPath
67 | self.optionalValueKeyPath = nil
68 | self.fileID = fileID
69 | self.function = function
70 | self.line = line
71 | self.column = column
72 |
73 | let stateKeyPathString = String(describing: stateKeyPath)
74 | let valueTypeCharacterCount = String(describing: Value.self).count
75 | var valueKeyPathString = String(describing: valueKeyPath)
76 |
77 | valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)
78 |
79 | self.sliceKeyPath = "\(stateKeyPathString)\(valueKeyPathString)"
80 | }
81 |
82 | /**
83 | Initializes a OptionalConstant with the provided parameters. This constructor is used to create a OptionalConstant that provides access to a specific part of an AppState's state. It provides granular control over the AppState.
84 |
85 | - Parameters:
86 | - stateKeyPath: A KeyPath that points to the state in AppState that should be sliced.
87 | - valueKeyPath: A KeyPath that points to the specific part of the state that should be accessed.
88 | */
89 | @MainActor
90 | public init(
91 | _ stateKeyPath: KeyPath,
92 | _ optionalValueKeyPath: KeyPath,
93 | _ fileID: StaticString = #fileID,
94 | _ function: StaticString = #function,
95 | _ line: Int = #line,
96 | _ column: Int = #column
97 | ) {
98 | self.stateKeyPath = stateKeyPath
99 | self.valueKeyPath = nil
100 | self.optionalValueKeyPath = optionalValueKeyPath
101 | self.fileID = fileID
102 | self.function = function
103 | self.line = line
104 | self.column = column
105 |
106 | let stateKeyPathString = String(describing: stateKeyPath)
107 | let valueTypeCharacterCount = String(describing: Value.self).count
108 | var valueKeyPathString = String(describing: optionalValueKeyPath)
109 |
110 | valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)
111 |
112 | self.sliceKeyPath = "\(stateKeyPathString)\(valueKeyPathString)"
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/Sources/AppState/PropertyWrappers/State/Slice/OptionalSlice.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import Combine
3 | import SwiftUI
4 | #endif
5 |
6 | /// A property wrapper that provides access to a specific part of the AppState's state that is optional.
7 | @propertyWrapper public struct OptionalSlice where SlicedState.Value == Value? {
8 | #if !os(Linux) && !os(Windows)
9 | /// Holds the singleton instance of `Application`.
10 | @ObservedObject private var app: Application = Application.shared
11 | #else
12 | /// Holds the singleton instance of `Application`.
13 | @MainActor
14 | private var app: Application = Application.shared
15 | #endif
16 |
17 | /// Path for accessing `State` from Application.
18 | private let stateKeyPath: KeyPath
19 |
20 | /// Path for accessing `SliceValue` from `Value`.
21 | private let valueKeyPath: WritableKeyPath?
22 |
23 | /// Path for accessing `SliceValue?` from `Value`.
24 | private let optionalValueKeyPath: WritableKeyPath?
25 |
26 | private let fileID: StaticString
27 | private let function: StaticString
28 | private let line: Int
29 | private let column: Int
30 | private let sliceKeyPath: String
31 |
32 | /// Represents the current value of the `State`.
33 | @MainActor
34 | public var wrappedValue: SliceValue? {
35 | get {
36 | if let valueKeyPath {
37 | return Application.slice(
38 | stateKeyPath,
39 | valueKeyPath,
40 | fileID,
41 | function,
42 | line,
43 | column
44 | ).value
45 | }
46 |
47 | guard
48 | let optionalValueKeyPath,
49 | let slicedValue = Application.slice(
50 | stateKeyPath,
51 | optionalValueKeyPath,
52 | fileID,
53 | function,
54 | line,
55 | column
56 | ).value
57 | else {
58 | return nil
59 | }
60 |
61 | return slicedValue
62 | }
63 | nonmutating set {
64 | Application.log(
65 | debug: {
66 | if let newValue {
67 | return "🍕 Setting Slice \(sliceKeyPath) = \(newValue)"
68 | } else {
69 | return "🍕 Setting Slice \(sliceKeyPath) = nil"
70 | }
71 | },
72 | fileID: fileID,
73 | function: function,
74 | line: line,
75 | column: column
76 | )
77 |
78 | var state = app.value(keyPath: stateKeyPath)
79 |
80 | guard
81 | let valueKeyPath,
82 | let newValue
83 | else {
84 | if let optionalValueKeyPath {
85 | state.value?[keyPath: optionalValueKeyPath] = newValue
86 | }
87 |
88 | return
89 | }
90 |
91 | state.value?[keyPath: valueKeyPath] = newValue
92 | }
93 | }
94 |
95 | #if !os(Linux) && !os(Windows)
96 | /// A binding to the `State`'s value, which can be used with SwiftUI views.
97 | @MainActor
98 | public var projectedValue: Binding {
99 | Binding(
100 | get: { wrappedValue },
101 | set: { wrappedValue = $0 }
102 | )
103 | }
104 | #endif
105 |
106 | /**
107 | Initializes a Slice with the provided parameters. This constructor is used to create a Slice that provides access and modification to a specific part of an AppState's state. It provides granular control over the AppState.
108 |
109 | - Parameters:
110 | - stateKeyPath: A KeyPath that points to the state in AppState that should be sliced.
111 | - valueKeyPath: A WritableKeyPath that points to the specific part of the state that should be accessed.
112 | */
113 | @MainActor
114 | public init(
115 | _ stateKeyPath: KeyPath,
116 | _ valueKeyPath: WritableKeyPath,
117 | _ fileID: StaticString = #fileID,
118 | _ function: StaticString = #function,
119 | _ line: Int = #line,
120 | _ column: Int = #column
121 | ) {
122 | self.stateKeyPath = stateKeyPath
123 | self.valueKeyPath = valueKeyPath
124 | self.optionalValueKeyPath = nil
125 | self.fileID = fileID
126 | self.function = function
127 | self.line = line
128 | self.column = column
129 |
130 | let stateKeyPathString = String(describing: stateKeyPath)
131 | let valueTypeCharacterCount = String(describing: Value.self).count
132 | var valueKeyPathString = String(describing: valueKeyPath)
133 |
134 | valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)
135 |
136 | self.sliceKeyPath = "\(stateKeyPathString)\(valueKeyPathString)"
137 | }
138 |
139 | /**
140 | Initializes a Slice with the provided parameters. This constructor is used to create a Slice that provides access and modification to a specific part of an AppState's state. It provides granular control over the AppState.
141 |
142 | - Parameters:
143 | - stateKeyPath: A KeyPath that points to the state in AppState that should be sliced.
144 | - valueKeyPath: A WritableKeyPath that points to the specific part of the state that should be accessed. Must be Optional.
145 | */
146 | @MainActor
147 | public init(
148 | _ stateKeyPath: KeyPath,
149 | _ optionalValueKeyPath: WritableKeyPath,
150 | _ fileID: StaticString = #fileID,
151 | _ function: StaticString = #function,
152 | _ line: Int = #line,
153 | _ column: Int = #column
154 | ) {
155 | self.stateKeyPath = stateKeyPath
156 | self.valueKeyPath = nil
157 | self.optionalValueKeyPath = optionalValueKeyPath
158 | self.fileID = fileID
159 | self.function = function
160 | self.line = line
161 | self.column = column
162 |
163 | let stateKeyPathString = String(describing: stateKeyPath)
164 | let valueTypeCharacterCount = String(describing: Value.self).count
165 | var valueKeyPathString = String(describing: optionalValueKeyPath)
166 |
167 | valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)
168 |
169 | self.sliceKeyPath = "\(stateKeyPathString)\(valueKeyPathString)"
170 | }
171 |
172 | #if !os(Linux) && !os(Windows)
173 | /// A property wrapper's synthetic storage property. This is just for SwiftUI to mutate the `wrappedValue` and send event through `objectWillChange` publisher when the `wrappedValue` changes
174 | @MainActor
175 | public static subscript(
176 | _enclosingInstance observed: OuterSelf,
177 | wrapped wrappedKeyPath: ReferenceWritableKeyPath,
178 | storage storageKeyPath: ReferenceWritableKeyPath
179 | ) -> SliceValue? {
180 | get {
181 | observed[keyPath: storageKeyPath].wrappedValue
182 | }
183 | set {
184 | guard
185 | let publisher = observed.objectWillChange as? ObservableObjectPublisher
186 | else { return }
187 |
188 | publisher.send()
189 | observed[keyPath: storageKeyPath].wrappedValue = newValue
190 | }
191 | }
192 | #endif
193 | }
194 |
195 | #if !os(Linux) && !os(Windows)
196 | extension OptionalSlice: DynamicProperty { }
197 | #endif
198 |
--------------------------------------------------------------------------------
/Sources/AppState/PropertyWrappers/State/Slice/Slice.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import Combine
3 | import SwiftUI
4 | #endif
5 |
6 | /// A property wrapper that provides access to a specific part of the AppState's state.
7 | @propertyWrapper public struct Slice where SlicedState.Value == Value {
8 | #if !os(Linux) && !os(Windows)
9 | /// Holds the singleton instance of `Application`.
10 | @ObservedObject private var app: Application = Application.shared
11 | #else
12 | /// Holds the singleton instance of `Application`.
13 | @MainActor
14 | private var app: Application = Application.shared
15 | #endif
16 |
17 | /// Path for accessing `State` from Application.
18 | private let stateKeyPath: KeyPath
19 |
20 | /// Path for accessing `SliceValue` from `Value`.
21 | private let valueKeyPath: WritableKeyPath
22 |
23 | private let fileID: StaticString
24 | private let function: StaticString
25 | private let line: Int
26 | private let column: Int
27 | private let sliceKeyPath: String
28 |
29 | /// Represents the current value of the `State`.
30 | @MainActor
31 | public var wrappedValue: SliceValue {
32 | get {
33 | Application.slice(
34 | stateKeyPath,
35 | valueKeyPath,
36 | fileID,
37 | function,
38 | line,
39 | column
40 | ).value
41 | }
42 | nonmutating set {
43 | Application.log(
44 | debug: "🍕 Setting Slice \(sliceKeyPath) = \(newValue)",
45 | fileID: fileID,
46 | function: function,
47 | line: line,
48 | column: column
49 | )
50 |
51 | var state = app.value(keyPath: stateKeyPath)
52 | state.value[keyPath: valueKeyPath] = newValue
53 | }
54 | }
55 |
56 | #if !os(Linux) && !os(Windows)
57 | /// A binding to the `State`'s value, which can be used with SwiftUI views.
58 | @MainActor
59 | public var projectedValue: Binding {
60 | Binding(
61 | get: { wrappedValue },
62 | set: { wrappedValue = $0 }
63 | )
64 | }
65 | #endif
66 |
67 | /**
68 | Initializes a Slice with the provided parameters. This constructor is used to create a Slice that provides access and modification to a specific part of an AppState's state. It provides granular control over the AppState.
69 |
70 | - Parameters:
71 | - stateKeyPath: A KeyPath that points to the state in AppState that should be sliced.
72 | - valueKeyPath: A WritableKeyPath that points to the specific part of the state that should be accessed.
73 | */
74 | @MainActor
75 | public init(
76 | _ stateKeyPath: KeyPath,
77 | _ valueKeyPath: WritableKeyPath,
78 | _ fileID: StaticString = #fileID,
79 | _ function: StaticString = #function,
80 | _ line: Int = #line,
81 | _ column: Int = #column
82 | ) {
83 | self.stateKeyPath = stateKeyPath
84 | self.valueKeyPath = valueKeyPath
85 | self.fileID = fileID
86 | self.function = function
87 | self.line = line
88 | self.column = column
89 |
90 | let stateKeyPathString = String(describing: stateKeyPath)
91 | let valueTypeCharacterCount = String(describing: Value.self).count
92 | var valueKeyPathString = String(describing: valueKeyPath)
93 |
94 | valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)
95 |
96 | self.sliceKeyPath = "\(stateKeyPathString)\(valueKeyPathString)"
97 | }
98 |
99 | #if !os(Linux) && !os(Windows)
100 | /// A property wrapper's synthetic storage property. This is just for SwiftUI to mutate the `wrappedValue` and send event through `objectWillChange` publisher when the `wrappedValue` changes
101 | @MainActor
102 | public static subscript(
103 | _enclosingInstance observed: OuterSelf,
104 | wrapped wrappedKeyPath: ReferenceWritableKeyPath,
105 | storage storageKeyPath: ReferenceWritableKeyPath
106 | ) -> SliceValue {
107 | get {
108 | observed[keyPath: storageKeyPath].wrappedValue
109 | }
110 | set {
111 | guard
112 | let publisher = observed.objectWillChange as? ObservableObjectPublisher
113 | else { return }
114 |
115 | publisher.send()
116 | observed[keyPath: storageKeyPath].wrappedValue = newValue
117 | }
118 | }
119 | #endif
120 | }
121 |
122 | #if !os(Linux) && !os(Windows)
123 | extension Slice: DynamicProperty { }
124 | #endif
125 |
--------------------------------------------------------------------------------
/Sources/AppState/PropertyWrappers/State/StoredState.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if !os(Linux) && !os(Windows)
3 | import Combine
4 | import SwiftUI
5 | #endif
6 |
7 | /// `StoredState` is a property wrapper allowing SwiftUI views to subscribe to Application's state changes in a reactive way. State is stored using `UserDefaults`. Works similar to `State` and `Published`.
8 | @propertyWrapper public struct StoredState {
9 | #if !os(Linux) && !os(Windows)
10 | /// Holds the singleton instance of `Application`.
11 | @ObservedObject private var app: Application = Application.shared
12 | #else
13 | /// Holds the singleton instance of `Application`.
14 | @MainActor
15 | private var app: Application = Application.shared
16 | #endif
17 |
18 | /// Path for accessing `StoredState` from Application.
19 | private let keyPath: KeyPath>
20 |
21 | private let fileID: StaticString
22 | private let function: StaticString
23 | private let line: Int
24 | private let column: Int
25 |
26 | /// Represents the current value of the `StoredState`.
27 | @MainActor
28 | public var wrappedValue: Value {
29 | get {
30 | Application.storedState(
31 | keyPath,
32 | fileID,
33 | function,
34 | line,
35 | column
36 | ).value
37 | }
38 | nonmutating set {
39 | Application.log(
40 | debug: "💾 Setting StoredState \(String(describing: keyPath)) = \(newValue)",
41 | fileID: fileID,
42 | function: function,
43 | line: line,
44 | column: column
45 | )
46 |
47 | var state = app.value(keyPath: keyPath)
48 | state.value = newValue
49 | }
50 | }
51 |
52 | #if !os(Linux) && !os(Windows)
53 | /// A binding to the `State`'s value, which can be used with SwiftUI views.
54 | @MainActor
55 | public var projectedValue: Binding {
56 | Binding(
57 | get: { wrappedValue },
58 | set: { wrappedValue = $0 }
59 | )
60 | }
61 | #endif
62 |
63 | /**
64 | Initializes the AppState with a `keyPath` for accessing `StoredState` in Application.
65 |
66 | - Parameter keyPath: The `KeyPath` for accessing `StoredState` in Application.
67 | */
68 | @MainActor
69 | public init(
70 | _ keyPath: KeyPath>,
71 | _ fileID: StaticString = #fileID,
72 | _ function: StaticString = #function,
73 | _ line: Int = #line,
74 | _ column: Int = #column
75 | ) {
76 | self.keyPath = keyPath
77 | self.fileID = fileID
78 | self.function = function
79 | self.line = line
80 | self.column = column
81 | }
82 |
83 | #if !os(Linux) && !os(Windows)
84 | /// A property wrapper's synthetic storage property. This is just for SwiftUI to mutate the `wrappedValue` and send event through `objectWillChange` publisher when the `wrappedValue` changes
85 | @MainActor
86 | public static subscript(
87 | _enclosingInstance observed: OuterSelf,
88 | wrapped wrappedKeyPath: ReferenceWritableKeyPath,
89 | storage storageKeyPath: ReferenceWritableKeyPath
90 | ) -> Value {
91 | get {
92 | observed[keyPath: storageKeyPath].wrappedValue
93 | }
94 | set {
95 | guard
96 | let publisher = observed.objectWillChange as? ObservableObjectPublisher
97 | else { return }
98 |
99 | publisher.send()
100 | observed[keyPath: storageKeyPath].wrappedValue = newValue
101 | }
102 | }
103 | #endif
104 | }
105 |
106 | #if !os(Linux) && !os(Windows)
107 | extension StoredState: DynamicProperty { }
108 | #endif
109 |
--------------------------------------------------------------------------------
/Sources/AppState/PropertyWrappers/State/SyncState.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import Foundation
3 | import Combine
4 | import SwiftUI
5 |
6 | /**
7 | `SyncState` is a property wrapper that allows SwiftUI views to subscribe to Application's state changes in a reactive way. The state is synchronized using `NSUbiquitousKeyValueStore`, and it works similarly to `State` and `Published`.
8 |
9 | Changes your app writes to the key-value store object are initially held in memory, then written to disk by the system at appropriate times. If you write to the key-value store object when the user is not signed into an iCloud account, the data is stored locally until the next synchronization opportunity. When the user signs into an iCloud account, the system automatically reconciles your local, on-disk keys and values with those on the iCloud server.
10 |
11 | The total amount of space available in your app’s key-value store, for a given user, is 1 MB. There is a per-key value size limit of 1 MB, and a maximum of 1024 keys. If you attempt to write data that exceeds these quotas, the write attempt fails and no change is made to your iCloud key-value storage. In this scenario, the system posts the didChangeExternallyNotification notification with a change reason of NSUbiquitousKeyValueStoreQuotaViolationChange.
12 |
13 | - Note: The key-value store is intended for storing data that changes infrequently. As you test your devices, if the app on a device makes frequent changes to the key-value store, the system may defer the synchronization of some changes in order to minimize the number of round trips to the server. The more frequently the app make changes, the more likely the changes will be deferred and will not immediately show up on the other devices.
14 |
15 | The maximum length for key strings for the iCloud key-value store is 64 bytes using UTF8 encoding. Attempting to write a value to a longer key name results in a runtime error.
16 |
17 | To use this class, you must distribute your app through the App Store or Mac App Store, and you must request the com.apple.developer.ubiquity-kvstore-identifier entitlement in your Xcode project.
18 |
19 | - Warning: Avoid using this class for data that is essential to your app’s behavior when offline; instead, store such data directly into the local user defaults database.
20 | */
21 | @available(watchOS 9.0, *)
22 | @propertyWrapper public struct SyncState: DynamicProperty {
23 | /// Holds the singleton instance of `Application`.
24 | @ObservedObject private var app: Application = Application.shared
25 |
26 | /// Path for accessing `SyncState` from Application.
27 | private let keyPath: KeyPath>
28 |
29 | private let fileID: StaticString
30 | private let function: StaticString
31 | private let line: Int
32 | private let column: Int
33 |
34 | /// Represents the current value of the `SyncState`.
35 | @MainActor
36 | public var wrappedValue: Value {
37 | get {
38 | Application.syncState(
39 | keyPath,
40 | fileID,
41 | function,
42 | line,
43 | column
44 | ).value
45 | }
46 | nonmutating set {
47 | Application.log(
48 | debug: "☁️ Setting SyncState \(String(describing: keyPath)) = \(newValue)",
49 | fileID: fileID,
50 | function: function,
51 | line: line,
52 | column: column
53 | )
54 |
55 | var state = app.value(keyPath: keyPath)
56 | state.value = newValue
57 | }
58 | }
59 |
60 | /// A binding to the `State`'s value, which can be used with SwiftUI views.
61 | @MainActor
62 | public var projectedValue: Binding {
63 | Binding(
64 | get: { wrappedValue },
65 | set: { wrappedValue = $0 }
66 | )
67 | }
68 |
69 | /**
70 | Initializes the AppState with a `keyPath` for accessing `SyncState` in Application.
71 |
72 | - Parameter keyPath: The `KeyPath` for accessing `SyncState` in Application.
73 | */
74 | @MainActor
75 | public init(
76 | _ keyPath: KeyPath>,
77 | _ fileID: StaticString = #fileID,
78 | _ function: StaticString = #function,
79 | _ line: Int = #line,
80 | _ column: Int = #column
81 | ) {
82 | self.keyPath = keyPath
83 | self.fileID = fileID
84 | self.function = function
85 | self.line = line
86 | self.column = column
87 | }
88 |
89 | /// A property wrapper's synthetic storage property. This is just for SwiftUI to mutate the `wrappedValue` and send event through `objectWillChange` publisher when the `wrappedValue` changes
90 | @MainActor
91 | public static subscript(
92 | _enclosingInstance observed: OuterSelf,
93 | wrapped wrappedKeyPath: ReferenceWritableKeyPath,
94 | storage storageKeyPath: ReferenceWritableKeyPath
95 | ) -> Value {
96 | get {
97 | observed[keyPath: storageKeyPath].wrappedValue
98 | }
99 | set {
100 | guard
101 | let publisher = observed.objectWillChange as? ObservableObjectPublisher
102 | else { return }
103 |
104 | publisher.send()
105 | observed[keyPath: storageKeyPath].wrappedValue = newValue
106 | }
107 | }
108 | }
109 | #endif
110 |
--------------------------------------------------------------------------------
/Tests/AppStateTests/AppDependencyTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import AppState
3 |
4 | fileprivate protocol Networking: Sendable {
5 | func fetch()
6 | }
7 |
8 | fileprivate final class NetworkService: Networking {
9 | func fetch() {
10 | fatalError()
11 | }
12 | }
13 |
14 | fileprivate final class MockNetworking: Networking {
15 | func fetch() { /* no-op */ }
16 | }
17 |
18 | @MainActor
19 | fileprivate class ComposableService {
20 | @AppDependency(\.networking) var networking: Networking
21 | }
22 |
23 | fileprivate extension Application {
24 | var networking: Dependency {
25 | dependency(NetworkService())
26 | }
27 |
28 | @MainActor
29 | var composableService: Dependency {
30 | dependency(ComposableService())
31 | }
32 | }
33 |
34 | fileprivate struct ExampleDependencyWrapper {
35 | @AppDependency(\.networking) private var networking
36 |
37 | @MainActor
38 | func fetch() {
39 | networking.fetch()
40 | }
41 | }
42 |
43 | @MainActor
44 | final class AppDependencyTests: XCTestCase {
45 | override func setUp() async throws {
46 | Application
47 | .logging(isEnabled: true)
48 | .promote(\.networking, with: MockNetworking())
49 | }
50 |
51 | override func tearDown() async throws {
52 | let applicationDescription = Application.description
53 |
54 | Application.logger.debug("AppDependencyTests \(applicationDescription)")
55 | }
56 |
57 | func testComposableDependencies() {
58 | let composableService = Application.dependency(\.composableService)
59 |
60 | composableService.networking.fetch()
61 | }
62 |
63 | func testDependency() async {
64 | Application.promote(\.networking, with: NetworkService())
65 |
66 | let networkingOverride = Application.override(\.networking, with: MockNetworking())
67 |
68 | let mockNetworking = Application.dependency(\.networking)
69 |
70 | XCTAssertNotNil(mockNetworking as? MockNetworking)
71 |
72 | mockNetworking.fetch()
73 |
74 | let example = ExampleDependencyWrapper()
75 |
76 | example.fetch()
77 |
78 | await networkingOverride.cancel()
79 |
80 | let networkingService = Application.dependency(\.networking)
81 |
82 | XCTAssertNotNil(networkingService as? NetworkService)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Tests/AppStateTests/AppStateTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if !os(Linux) && !os(Windows)
3 | import SwiftUI
4 | #endif
5 | import XCTest
6 | @testable import AppState
7 |
8 | fileprivate extension Application {
9 | var isLoading: State {
10 | state(initial: false)
11 | }
12 |
13 | var username: State {
14 | state(initial: "Leif")
15 | }
16 |
17 | var date: State {
18 | state(initial: Date())
19 | }
20 |
21 | var colors: State<[String: String]> {
22 | state(initial: ["primary": "#A020F0"])
23 | }
24 | }
25 |
26 | @MainActor
27 | fileprivate class ExampleViewModel {
28 | @AppState(\.username) var username
29 |
30 | func testPropertyWrapper() {
31 | username = "Hello, ExampleView"
32 | }
33 | }
34 |
35 | #if !os(Linux) && !os(Windows)
36 | extension ExampleViewModel: ObservableObject { }
37 |
38 | fileprivate struct ExampleView: View {
39 | @AppState(\.username) var username
40 | @AppState(\.isLoading) var isLoading
41 |
42 | func testPropertyWrappers() {
43 | username = "Hello, ExampleView"
44 | #if !os(Linux) && !os(Windows)
45 | _ = Toggle(isOn: $isLoading) {
46 | Text("Is Loading")
47 | }
48 | #endif
49 | }
50 |
51 | var body: some View { EmptyView() }
52 | }
53 | #else
54 | @MainActor
55 | fileprivate struct ExampleView {
56 | @AppState(\.username) var username
57 | @AppState(\.isLoading) var isLoading
58 |
59 | func testPropertyWrappers() {
60 | username = "Hello, ExampleView"
61 | }
62 | }
63 | #endif
64 |
65 | final class AppStateTests: XCTestCase {
66 | @MainActor
67 | override func setUp() async throws {
68 | Application.logging(isEnabled: true)
69 | }
70 |
71 | @MainActor
72 | override func tearDown() async throws {
73 | let applicationDescription = Application.description
74 | Application.logger.debug("AppStateTests \(applicationDescription)")
75 |
76 | var username: Application.State = Application.state(\.username)
77 |
78 | username.value = "Leif"
79 | }
80 |
81 | @MainActor
82 | func testState() async {
83 | var appState: Application.State = Application.state(\.username)
84 |
85 | XCTAssertEqual(appState.value, "Leif")
86 |
87 | appState.value = "0xL"
88 |
89 | XCTAssertEqual(appState.value, "0xL")
90 | XCTAssertEqual(Application.state(\.username).value, "0xL")
91 | }
92 |
93 | @MainActor
94 | func testStateClosureCachesValueOnGet() async {
95 | let dateState: Application.State = Application.state(\.date)
96 |
97 | let copyOfDateState: Application.State = Application.state(\.date)
98 |
99 | XCTAssertEqual(copyOfDateState.value, dateState.value)
100 | }
101 |
102 | @MainActor
103 | func testPropertyWrappers() async {
104 | let exampleView = ExampleView()
105 |
106 | XCTAssertEqual(exampleView.username, "Leif")
107 |
108 | exampleView.testPropertyWrappers()
109 |
110 | XCTAssertEqual(exampleView.username, "Hello, ExampleView")
111 |
112 | let viewModel = ExampleViewModel()
113 |
114 | XCTAssertEqual(viewModel.username, "Hello, ExampleView")
115 |
116 | viewModel.username = "Hello, ViewModel"
117 |
118 | XCTAssertEqual(viewModel.username, "Hello, ViewModel")
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Tests/AppStateTests/ApplicationTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import AppState
3 |
4 | fileprivate class SomeApplication: Application {
5 | static func someFunction() { /* no-op */ }
6 | }
7 |
8 | final class ApplicationTests: XCTestCase {
9 | func testCustomFunction() async throws {
10 | let applicationType = await Application.logging(isEnabled: true)
11 | .load(dependency: \.userDefaults)
12 | .promote(to: SomeApplication.self)
13 |
14 | applicationType.someFunction()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/AppStateTests/DependencySliceTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if !os(Linux) && !os(Windows)
3 | import SwiftUI
4 | #endif
5 | import XCTest
6 | @testable import AppState
7 |
8 | @MainActor
9 | fileprivate class ExampleViewModel: Sendable {
10 | var username: String? = nil
11 | var isLoading: Bool = false
12 | let value: String = "Hello, World!"
13 | var mutableValue: String = "..."
14 | }
15 |
16 | #if !os(Linux) && !os(Windows)
17 | extension ExampleViewModel: ObservableObject { }
18 | #endif
19 |
20 | fileprivate extension Application {
21 | var exampleViewModel: Dependency {
22 | dependency(ExampleViewModel())
23 | }
24 | }
25 |
26 | @MainActor
27 | fileprivate struct ExampleView {
28 | @DependencySlice(\.exampleViewModel, \.username) var username
29 | @DependencySlice(\.exampleViewModel, \.isLoading) var isLoading
30 | @DependencyConstant(\.exampleViewModel, \.value) var constantValue
31 | @DependencyConstant(\.exampleViewModel, \.mutableValue) var constantMutableValue
32 |
33 | func testPropertyWrappers() {
34 | username = "Hello, ExampleView"
35 | #if !os(Linux) && !os(Windows)
36 | _ = Toggle(isOn: $isLoading) {
37 | Text(constantMutableValue)
38 | }
39 | #endif
40 | }
41 | }
42 |
43 | final class DependencySliceTests: XCTestCase {
44 | @MainActor
45 | override func setUp() async throws {
46 | Application.logging(isEnabled: true)
47 | }
48 |
49 | @MainActor
50 | override func tearDown() async throws {
51 | let applicationDescription = Application.description
52 |
53 | Application.logger.debug("DependencySliceTests \(applicationDescription)")
54 | }
55 |
56 | @MainActor
57 | func testApplicationSliceFunction() async {
58 | var exampleSlice = Application.dependencySlice(\.exampleViewModel, \.username)
59 |
60 | exampleSlice.value = "New Value!"
61 |
62 | XCTAssertEqual(exampleSlice.value, "New Value!")
63 | XCTAssertEqual(Application.dependencySlice(\.exampleViewModel, \.username).value, "New Value!")
64 | XCTAssertEqual(Application.dependency(\.exampleViewModel).username, "New Value!")
65 |
66 | exampleSlice.value = "Leif"
67 |
68 | XCTAssertEqual(exampleSlice.value, "Leif")
69 | XCTAssertEqual(Application.dependencySlice(\.exampleViewModel, \.username).value, "Leif")
70 | XCTAssertEqual(Application.dependency(\.exampleViewModel).username, "Leif")
71 | }
72 |
73 | @MainActor
74 | func testPropertyWrappers() async {
75 | let exampleView = ExampleView()
76 |
77 | XCTAssertEqual(exampleView.username, "Leif")
78 |
79 | exampleView.testPropertyWrappers()
80 |
81 | XCTAssertEqual(exampleView.username, "Hello, ExampleView")
82 | XCTAssertEqual(exampleView.constantValue, "Hello, World!")
83 | XCTAssertEqual(exampleView.constantMutableValue, "...")
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Tests/AppStateTests/FileStateTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if !os(Linux) && !os(Windows)
3 | import SwiftUI
4 | #endif
5 | import XCTest
6 | @testable import AppState
7 |
8 | fileprivate extension Application {
9 | @MainActor
10 | var storedValue: FileState {
11 | fileState(filename: "storedValue")
12 | }
13 |
14 | @MainActor
15 | var storedString: FileState {
16 | fileState(filename: "storedString", isBase64Encoded: false)
17 | }
18 | }
19 |
20 | @MainActor
21 | fileprivate struct ExampleStoredValue {
22 | @FileState(\.storedValue) var count
23 | @FileState(\.storedString) var storedString
24 | }
25 |
26 | @MainActor
27 | fileprivate class ExampleStoringViewModel {
28 | @FileState(\.storedValue) var count
29 | @FileState(\.storedString) var storedString
30 |
31 | func testPropertyWrapper() {
32 | count = 27
33 | storedString = "Hello"
34 | #if !os(Linux) && !os(Windows)
35 | _ = TextField(
36 | value: $count,
37 | format: .number,
38 | label: { Text("Count") }
39 | )
40 | #endif
41 | }
42 | }
43 |
44 | #if !os(Linux) && !os(Windows)
45 | extension ExampleStoringViewModel: ObservableObject { }
46 | #endif
47 |
48 | final class FileStateTests: XCTestCase {
49 | @MainActor
50 | override func setUp() async throws {
51 | Application.logging(isEnabled: true)
52 |
53 | FileManager.defaultFileStatePath = "./AppStateTests"
54 | }
55 |
56 | @MainActor
57 | override func tearDown() async throws {
58 | let applicationDescription = Application.description
59 |
60 | Application.logger.debug("FileStateTests \(applicationDescription)")
61 |
62 | try? Application.dependency(\.fileManager).removeItem(atPath: "./AppStateTests")
63 | }
64 |
65 | @MainActor
66 | func testFileState() async {
67 | XCTAssertEqual(FileManager.defaultFileStatePath, "./AppStateTests")
68 | XCTAssertNil(Application.fileState(\.storedValue).value)
69 | XCTAssertNil(Application.fileState(\.storedString).value)
70 |
71 | let storedValue = ExampleStoredValue()
72 |
73 | XCTAssertEqual(storedValue.count, nil)
74 | XCTAssertEqual(storedValue.storedString, nil)
75 |
76 | storedValue.count = 1
77 | storedValue.storedString = "Hello"
78 |
79 | XCTAssertEqual(storedValue.count, 1)
80 | XCTAssertEqual(storedValue.storedString, "Hello")
81 |
82 | Application.logger.debug("FileStateTests \(Application.description)")
83 |
84 | storedValue.count = nil
85 | storedValue.storedString = nil
86 |
87 | XCTAssertNil(Application.fileState(\.storedValue).value)
88 | XCTAssertNil(Application.fileState(\.storedString).value)
89 | }
90 |
91 | @MainActor
92 | func testStoringViewModel() async {
93 | XCTAssertEqual(FileManager.defaultFileStatePath, "./AppStateTests")
94 | XCTAssertNil(Application.fileState(\.storedValue).value)
95 | XCTAssertNil(Application.fileState(\.storedString).value)
96 |
97 | let viewModel = ExampleStoringViewModel()
98 |
99 | XCTAssertEqual(viewModel.count, nil)
100 | XCTAssertEqual(viewModel.storedString, nil)
101 |
102 | viewModel.testPropertyWrapper()
103 |
104 | XCTAssertEqual(viewModel.count, 27)
105 | XCTAssertEqual(viewModel.storedString, "Hello")
106 |
107 | Application.reset(fileState: \.storedValue)
108 | Application.reset(fileState: \.storedString)
109 |
110 | XCTAssertNil(viewModel.count)
111 | XCTAssertNil(viewModel.storedString)
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Tests/AppStateTests/KeychainTests.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import XCTest
3 | @testable import AppState
4 |
5 | final class KeychainTests: XCTestCase {
6 | @MainActor
7 | func testKeychainInitKeys() async throws {
8 | let keychain = Keychain(keys: ["key"])
9 |
10 | XCTAssertThrowsError(try keychain.resolve("key"))
11 | }
12 |
13 | @MainActor
14 | func testKeychainInitValues() async throws {
15 | let keychain = Keychain(keys: ["key"])
16 |
17 | keychain.set(value: "abc", forKey: "key")
18 |
19 | let value = try keychain.resolve("key")
20 |
21 | XCTAssertEqual(value, "abc")
22 |
23 | keychain.remove("key")
24 | }
25 |
26 | @MainActor
27 | func testKeychainContains() async throws {
28 | let keychain = Keychain()
29 |
30 | XCTAssertFalse(keychain.contains("key"))
31 |
32 | keychain.set(value: "abc", forKey: "key")
33 |
34 | XCTAssertTrue(keychain.contains("key"))
35 |
36 | keychain.remove("key")
37 | }
38 |
39 | @MainActor
40 | func testKeychainRequiresSuccess() async throws {
41 | let keychain = Keychain(keys: ["key"])
42 |
43 | keychain.set(value: "abs", forKey: "key")
44 |
45 | XCTAssertNoThrow(try keychain.require("key"))
46 |
47 | keychain.remove("key")
48 | }
49 |
50 | @MainActor
51 | func testKeychainRequiresFailure() async throws {
52 | let keychain = Keychain()
53 |
54 | XCTAssertThrowsError(try keychain.require("key"))
55 | }
56 |
57 | @MainActor
58 | func testKeychainValues() async throws {
59 | let keychain = Keychain(keys: ["key"])
60 |
61 | keychain.set(value: "abc", forKey: "key")
62 |
63 | let values = keychain.values()
64 | let secureValue = keychain.get("key")
65 |
66 | XCTAssertEqual(values.count, 1)
67 | XCTAssertNotNil(secureValue)
68 | XCTAssertEqual(secureValue, "abc")
69 |
70 | keychain.remove("key")
71 |
72 | XCTAssertEqual(keychain.values().count, 0)
73 | XCTAssertNil(keychain.get("key"))
74 | }
75 | }
76 | #endif
77 |
--------------------------------------------------------------------------------
/Tests/AppStateTests/ObservedDependencyTests.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import SwiftUI
3 | import XCTest
4 | @testable import AppState
5 |
6 | @MainActor
7 | fileprivate class ObservableService: ObservableObject {
8 | @Published var count: Int
9 |
10 | init() {
11 | count = 0
12 | }
13 | }
14 |
15 | fileprivate extension Application {
16 | var test: Dependency {
17 | dependency("!!!")
18 | }
19 |
20 | @MainActor
21 | var observableService: Dependency {
22 | dependency(ObservableService())
23 | }
24 | }
25 |
26 | @MainActor
27 | fileprivate struct ExampleDependencyWrapper {
28 | @ObservedDependency(\.observableService) var service
29 |
30 | func test() {
31 | service.count += 1
32 |
33 | _ = Picker("", selection: $service.count, content: EmptyView.init)
34 | }
35 | }
36 |
37 | final class ObservedDependencyTests: XCTestCase {
38 | @MainActor
39 | override func setUp() async throws {
40 | Application.logging(isEnabled: true)
41 | }
42 |
43 | @MainActor
44 | override func tearDown() async throws {
45 | let applicationDescription = Application.description
46 |
47 | Application.logger.debug("ObservedDependencyTests \(applicationDescription)")
48 | }
49 |
50 | @MainActor
51 | func testDependency() async {
52 | let example = ExampleDependencyWrapper()
53 |
54 | XCTAssertEqual(example.service.count, 0)
55 |
56 | example.test()
57 |
58 | XCTAssertEqual(example.service.count, 1)
59 | }
60 | }
61 | #endif
62 |
--------------------------------------------------------------------------------
/Tests/AppStateTests/OptionalSliceTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if !os(Linux) && !os(Windows)
3 | import SwiftUI
4 | #endif
5 | import XCTest
6 | @testable import AppState
7 |
8 | fileprivate struct ExampleValue {
9 | var username: String?
10 | var isLoading: Bool
11 | let value: String
12 | }
13 |
14 | fileprivate extension Application {
15 | var exampleValue: State {
16 | state(
17 | initial: ExampleValue(
18 | username: nil,
19 | isLoading: false,
20 | value: "value"
21 | )
22 | )
23 | }
24 | }
25 |
26 | @MainActor
27 | fileprivate class ExampleViewModel {
28 | @OptionalSlice(\.exampleValue, \.username) var username
29 | @OptionalConstant(\.exampleValue, \.value) var value
30 | @OptionalSlice(\.exampleValue, \.isLoading) var isLoading
31 | @OptionalConstant(\.exampleValue, \.username) var constantUsername
32 |
33 | func testPropertyWrapper() {
34 | username = "Hello, ExampleView"
35 | }
36 | }
37 |
38 | #if !os(Linux) && !os(Windows)
39 | extension ExampleViewModel: ObservableObject { }
40 | #endif
41 |
42 | @MainActor
43 | fileprivate struct ExampleView {
44 | @OptionalSlice(\.exampleValue, \.username) var username
45 | @OptionalSlice(\.exampleValue, \.isLoading) var isLoading
46 |
47 | func testPropertyWrappers() {
48 | username = "Hello, ExampleView"
49 | #if !os(Linux) && !os(Windows)
50 | _ = Picker("Picker", selection: $isLoading, content: EmptyView.init)
51 | #endif
52 | }
53 | }
54 |
55 | final class OptionalSliceTests: XCTestCase {
56 | @MainActor
57 | override func setUp() async throws {
58 | Application.logging(isEnabled: true)
59 | }
60 |
61 | @MainActor
62 | override func tearDown() async throws {
63 | let applicationDescription = Application.description
64 |
65 | Application.logger.debug("AppStateTests \(applicationDescription)")
66 | }
67 |
68 | @MainActor
69 | func testApplicationSliceFunction() async {
70 | var exampleSlice = Application.slice(\.exampleValue, \.username)
71 |
72 | exampleSlice.value = "New Value!"
73 |
74 | XCTAssertEqual(exampleSlice.value, "New Value!")
75 | XCTAssertEqual(Application.slice(\.exampleValue, \.username).value, "New Value!")
76 | XCTAssertEqual(Application.state(\.exampleValue).value?.username, "New Value!")
77 |
78 | exampleSlice.value = "Leif"
79 |
80 | XCTAssertEqual(exampleSlice.value, "Leif")
81 | XCTAssertEqual(Application.slice(\.exampleValue, \.username).value, "Leif")
82 | XCTAssertEqual(Application.state(\.exampleValue).value?.username, "Leif")
83 |
84 | exampleSlice.value = nil
85 | }
86 |
87 | @MainActor
88 | func testPropertyWrappers() async {
89 | let exampleView = ExampleView()
90 |
91 | exampleView.username = "Leif"
92 |
93 | XCTAssertEqual(exampleView.username, "Leif")
94 |
95 | exampleView.testPropertyWrappers()
96 |
97 | XCTAssertEqual(exampleView.username, "Hello, ExampleView")
98 |
99 | let viewModel = ExampleViewModel()
100 |
101 | XCTAssertEqual(viewModel.username, "Hello, ExampleView")
102 |
103 | viewModel.username = "Hello, ViewModel"
104 |
105 | XCTAssertEqual(viewModel.username, "Hello, ViewModel")
106 | XCTAssertEqual(viewModel.constantUsername, "Hello, ViewModel")
107 |
108 | viewModel.username = nil
109 |
110 | viewModel.isLoading = nil
111 |
112 | XCTAssertNil(viewModel.username)
113 | XCTAssertNotNil(viewModel.isLoading)
114 |
115 | XCTAssertEqual(viewModel.value, "value")
116 | }
117 |
118 | @MainActor
119 | func testNil() async {
120 | let viewModel = ExampleViewModel()
121 | viewModel.username = nil
122 | XCTAssertNil(viewModel.username)
123 | viewModel.username = "Leif"
124 | XCTAssertNotNil(viewModel.username)
125 | viewModel.username = nil
126 | XCTAssertNil(viewModel.username)
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Tests/AppStateTests/SecureStateTests.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import SwiftUI
3 | import XCTest
4 | @testable import AppState
5 |
6 | fileprivate extension Application {
7 | var secureValue: SecureState {
8 | secureState(id: "secureState")
9 | }
10 | }
11 |
12 | @MainActor
13 | fileprivate struct ExampleSecureValue {
14 | @SecureState(\.secureValue) var token: String?
15 | }
16 |
17 | @MainActor
18 | fileprivate class ExampleSecureViewModel: ObservableObject {
19 | @SecureState(\.secureValue) var token: String?
20 |
21 | func testPropertyWrapper() {
22 | token = "QWERTY"
23 | _ = Picker("Picker", selection: $token, content: EmptyView.init)
24 | }
25 | }
26 |
27 | final class SecureStateTests: XCTestCase {
28 | @MainActor
29 | override func setUp() async throws {
30 | Application
31 | .logging(isEnabled: true)
32 | .load(dependency: \.keychain)
33 | }
34 |
35 | @MainActor
36 | override func tearDown() async throws {
37 | let applicationDescription = Application.description
38 |
39 | Application.logger.debug("SecureStateTests \(applicationDescription)")
40 | }
41 |
42 | @MainActor
43 | func testSecureState() async {
44 | XCTAssertNil(Application.secureState(\.secureValue).value)
45 |
46 | let secureValue = ExampleSecureValue()
47 |
48 | XCTAssertEqual(secureValue.token, nil)
49 |
50 | secureValue.token = "QWERTY"
51 |
52 | XCTAssertEqual(secureValue.token, "QWERTY")
53 |
54 | secureValue.token = UUID().uuidString
55 |
56 | XCTAssertNotEqual(secureValue.token, "QWERTY")
57 |
58 | Application.logger.debug("SecureStateTests \(Application.description)")
59 |
60 | secureValue.token = nil
61 |
62 | XCTAssertNil(Application.secureState(\.secureValue).value)
63 | }
64 |
65 | @MainActor
66 | func testStoringViewModel() async {
67 | XCTAssertNil(Application.secureState(\.secureValue).value)
68 |
69 | let viewModel = ExampleSecureViewModel()
70 |
71 | XCTAssertEqual(viewModel.token, nil)
72 |
73 | viewModel.testPropertyWrapper()
74 |
75 | XCTAssertEqual(viewModel.token, "QWERTY")
76 |
77 | Application.reset(secureState: \.secureValue)
78 |
79 | XCTAssertNil(viewModel.token)
80 | }
81 | }
82 | #endif
83 |
--------------------------------------------------------------------------------
/Tests/AppStateTests/SliceTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if !os(Linux) && !os(Windows)
3 | import SwiftUI
4 | #endif
5 | import XCTest
6 | @testable import AppState
7 |
8 | fileprivate struct ExampleValue {
9 | var username: String?
10 | var isLoading: Bool
11 | let value: String
12 | var mutableValue: String
13 | }
14 |
15 | fileprivate extension Application {
16 | var exampleValue: State {
17 | state(
18 | initial: ExampleValue(
19 | username: "Leif",
20 | isLoading: false,
21 | value: "value",
22 | mutableValue: ""
23 | )
24 | )
25 | }
26 | }
27 |
28 | @MainActor
29 | fileprivate class ExampleViewModel {
30 | @Slice(\.exampleValue, \.username) var username
31 | @Constant(\.exampleValue, \.value) var value
32 |
33 | func testPropertyWrapper() {
34 | username = "Hello, ExampleView"
35 | }
36 | }
37 |
38 | #if !os(Linux) && !os(Windows)
39 | extension ExampleViewModel: ObservableObject { }
40 | #endif
41 |
42 | @MainActor
43 | fileprivate struct ExampleView {
44 | @Slice(\.exampleValue, \.username) var username
45 | @Slice(\.exampleValue, \.isLoading) var isLoading
46 | @Constant(\.exampleValue, \.mutableValue) var constantMutableValue
47 |
48 | func testPropertyWrappers() {
49 | username = "Hello, ExampleView"
50 | #if !os(Linux) && !os(Windows)
51 | _ = Toggle(isOn: $isLoading) {
52 | Text(constantMutableValue)
53 | }
54 | #endif
55 | }
56 | }
57 |
58 | final class SliceTests: XCTestCase {
59 | @MainActor
60 | override func setUp() async throws {
61 | Application.logging(isEnabled: true)
62 | }
63 |
64 | @MainActor
65 | override func tearDown() async throws {
66 | let applicationDescription = Application.description
67 |
68 | Application.logger.debug("AppStateTests \(applicationDescription)")
69 | }
70 |
71 | @MainActor
72 | func testApplicationSliceFunction() async {
73 | var exampleSlice = Application.slice(\.exampleValue, \.username)
74 |
75 | exampleSlice.value = "New Value!"
76 |
77 | XCTAssertEqual(exampleSlice.value, "New Value!")
78 | XCTAssertEqual(Application.slice(\.exampleValue, \.username).value, "New Value!")
79 | XCTAssertEqual(Application.state(\.exampleValue).value.username, "New Value!")
80 |
81 | exampleSlice.value = "Leif"
82 |
83 | XCTAssertEqual(exampleSlice.value, "Leif")
84 | XCTAssertEqual(Application.slice(\.exampleValue, \.username).value, "Leif")
85 | XCTAssertEqual(Application.state(\.exampleValue).value.username, "Leif")
86 | }
87 |
88 | @MainActor
89 | func testPropertyWrappers() async {
90 | let exampleView = ExampleView()
91 |
92 | XCTAssertEqual(exampleView.username, "Leif")
93 |
94 | exampleView.testPropertyWrappers()
95 |
96 | XCTAssertEqual(exampleView.username, "Hello, ExampleView")
97 |
98 | let viewModel = ExampleViewModel()
99 |
100 | XCTAssertEqual(viewModel.username, "Hello, ExampleView")
101 |
102 | viewModel.username = "Hello, ViewModel"
103 |
104 | XCTAssertEqual(viewModel.username, "Hello, ViewModel")
105 |
106 | XCTAssertEqual(viewModel.value, "value")
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Tests/AppStateTests/StoredStateTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if !os(Linux) && !os(Windows)
3 | import SwiftUI
4 | #endif
5 | import XCTest
6 | @testable import AppState
7 |
8 | fileprivate extension Application {
9 | var storedValue: StoredState {
10 | storedState(id: "storedValue")
11 | }
12 | }
13 |
14 | @MainActor
15 | fileprivate struct ExampleStoredValue {
16 | @StoredState(\.storedValue) var count
17 | }
18 |
19 | @MainActor
20 | fileprivate class ExampleStoringViewModel {
21 | @StoredState(\.storedValue) var count
22 |
23 | func testPropertyWrapper() {
24 | count = 27
25 | #if !os(Linux) && !os(Windows)
26 | _ = TextField(
27 | value: $count,
28 | format: .number,
29 | label: { Text("Count") }
30 | )
31 | #endif
32 | }
33 | }
34 |
35 | #if !os(Linux) && !os(Windows)
36 | extension ExampleStoringViewModel: ObservableObject { }
37 | #endif
38 |
39 | final class StoredStateTests: XCTestCase {
40 | @MainActor
41 | override func setUp() async throws {
42 | Application.logging(isEnabled: true)
43 | }
44 |
45 | @MainActor
46 | override func tearDown() async throws {
47 | let applicationDescription = Application.description
48 |
49 | Application.logger.debug("StoredStateTests \(applicationDescription)")
50 | }
51 |
52 | @MainActor
53 | func testStoredState() async {
54 | XCTAssertNil(Application.storedState(\.storedValue).value)
55 |
56 | let storedValue = ExampleStoredValue()
57 |
58 | XCTAssertEqual(storedValue.count, nil)
59 |
60 | storedValue.count = 1
61 |
62 | XCTAssertEqual(storedValue.count, 1)
63 |
64 | Application.logger.debug("StoredStateTests \(Application.description)")
65 |
66 | storedValue.count = nil
67 |
68 | XCTAssertNil(Application.storedState(\.storedValue).value)
69 | }
70 |
71 | @MainActor
72 | func testStoringViewModel() async {
73 | XCTAssertNil(Application.storedState(\.storedValue).value)
74 |
75 | let viewModel = ExampleStoringViewModel()
76 |
77 | XCTAssertEqual(viewModel.count, nil)
78 |
79 | viewModel.testPropertyWrapper()
80 |
81 | XCTAssertEqual(viewModel.count, 27)
82 |
83 | Application.reset(storedState: \.storedValue)
84 |
85 | XCTAssertNil(viewModel.count)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Tests/AppStateTests/SyncStateTests.swift:
--------------------------------------------------------------------------------
1 | #if !os(Linux) && !os(Windows)
2 | import SwiftUI
3 | import XCTest
4 | @testable import AppState
5 |
6 | @available(watchOS 9.0, *)
7 | fileprivate extension Application {
8 | var syncValue: SyncState {
9 | syncState(id: "syncValue")
10 | }
11 |
12 | var syncFailureValue: SyncState {
13 | syncState(initial: -1, id: "syncValue")
14 | }
15 | }
16 |
17 | @available(watchOS 9.0, *)
18 | @MainActor
19 | fileprivate struct ExampleSyncValue {
20 | @SyncState(\.syncValue) var count
21 | }
22 |
23 |
24 | @available(watchOS 9.0, *)
25 | @MainActor
26 | fileprivate struct ExampleFailureSyncValue {
27 | @SyncState(\.syncFailureValue) var count
28 | }
29 |
30 |
31 | @available(watchOS 9.0, *)
32 | @MainActor
33 | fileprivate class ExampleStoringViewModel: ObservableObject {
34 | @SyncState(\.syncValue) var count
35 |
36 | func testPropertyWrapper() {
37 | count = 27
38 | _ = TextField(
39 | value: $count,
40 | format: .number,
41 | label: { Text("Count") }
42 | )
43 | }
44 | }
45 |
46 |
47 | @available(watchOS 9.0, *)
48 | final class SyncStateTests: XCTestCase {
49 | @MainActor
50 | override func setUp() async throws {
51 | Application
52 | .logging(isEnabled: true)
53 | .load(dependency: \.icloudStore)
54 | }
55 |
56 | @MainActor
57 | override func tearDown() async throws {
58 | let applicationDescription = Application.description
59 |
60 | Application.logger.debug("SyncStateTests \(applicationDescription)")
61 | }
62 |
63 | @MainActor
64 | func testSyncState() async {
65 | XCTAssertNil(Application.syncState(\.syncValue).value)
66 |
67 | let syncValue = ExampleSyncValue()
68 |
69 | XCTAssertEqual(syncValue.count, nil)
70 |
71 | syncValue.count = 1
72 |
73 | XCTAssertEqual(syncValue.count, 1)
74 |
75 | Application.logger.debug("SyncStateTests \(Application.description)")
76 |
77 | syncValue.count = nil
78 |
79 | XCTAssertNil(Application.syncState(\.syncValue).value)
80 | }
81 |
82 | @MainActor
83 | func testFailEncodingSyncState() async{
84 | XCTAssertNotNil(Application.syncState(\.syncFailureValue).value)
85 |
86 | let syncValue = ExampleFailureSyncValue()
87 |
88 | XCTAssertEqual(syncValue.count, -1)
89 |
90 | syncValue.count = Double.infinity
91 |
92 | XCTAssertEqual(syncValue.count, Double.infinity)
93 | }
94 |
95 | @MainActor
96 | func testStoringViewModel() async {
97 | XCTAssertNil(Application.syncState(\.syncValue).value)
98 |
99 | let viewModel = ExampleStoringViewModel()
100 |
101 | XCTAssertEqual(viewModel.count, nil)
102 |
103 | viewModel.testPropertyWrapper()
104 |
105 | XCTAssertEqual(viewModel.count, 27)
106 |
107 | Application.reset(syncState: \.syncValue)
108 |
109 | XCTAssertNil(viewModel.count)
110 | }
111 | }
112 | #endif
113 |
--------------------------------------------------------------------------------
/documentation/advanced-usage.md:
--------------------------------------------------------------------------------
1 |
2 | # Advanced Usage of AppState
3 |
4 | This guide covers advanced topics for using **AppState**, including Just-In-Time creation, preloading dependencies, managing state and dependencies effectively, and comparing **AppState** with **SwiftUI's Environment**.
5 |
6 | ## 1. Just-In-Time Creation
7 |
8 | AppState values, such as `State`, `Dependency`, `StoredState`, and `SyncState`, are created just-in-time. This means they are instantiated only when first accessed, improving the efficiency and performance of your application.
9 |
10 | ### Example
11 |
12 | ```swift
13 | extension Application {
14 | var defaultState: State {
15 | state(initial: 0) // The value is not created until it's accessed
16 | }
17 | }
18 | ```
19 |
20 | In this example, `defaultState` is not created until it is accessed for the first time, optimizing resource usage.
21 |
22 | ## 2. Preloading Dependencies
23 |
24 | In some cases, you may want to preload certain dependencies to ensure they are available when your application starts. AppState provides a `load` function that preloads dependencies.
25 |
26 | ### Example
27 |
28 | ```swift
29 | extension Application {
30 | var databaseClient: Dependency {
31 | dependency(DatabaseClient())
32 | }
33 | }
34 |
35 | // Preload in app initialization
36 | Application.load(dependency: \.databaseClient)
37 | ```
38 |
39 | In this example, `databaseClient` is preloaded during the app's initialization, ensuring that it is available when needed in your views.
40 |
41 | ## 3. State and Dependency Management
42 |
43 | ### 3.1 Shared State and Dependencies Across the Application
44 |
45 | You can define shared state or dependencies in one part of your app and access them in another part using unique IDs.
46 |
47 | ### Example
48 |
49 | ```swift
50 | private extension Application {
51 | var stateValue: State {
52 | state(initial: 0, id: "stateValue")
53 | }
54 |
55 | var dependencyValue: Dependency {
56 | dependency(SomeType(), id: "dependencyValue")
57 | }
58 | }
59 | ```
60 |
61 | This allows you to access the same `State` or `Dependency` elsewhere by using the same ID.
62 |
63 | ```swift
64 | private extension Application {
65 | var theSameStateValue: State {
66 | state(initial: 0, id: "stateValue")
67 | }
68 |
69 | var theSameDependencyValue: Dependency {
70 | dependency(SomeType(), id: "dependencyValue")
71 | }
72 | }
73 | ```
74 |
75 | While this approach is valid for sharing state and dependencies across the application, it is not advised because it relies on manually managing IDs. This can lead to potential conflicts and bugs if IDs are not managed correctly. This behavior is more of a side effect of how the IDs work in AppState, rather than a recommended practice.
76 |
77 | ### 3.2 Restricted State and Dependency Access
78 |
79 | To restrict access, use a unique ID like a UUID to ensure that only the right parts of the app can access specific states or dependencies.
80 |
81 | ### Example
82 |
83 | ```swift
84 | private extension Application {
85 | var restrictedState: State {
86 | state(initial: nil, id: UUID().uuidString)
87 | }
88 |
89 | var restrictedDependency: Dependency {
90 | dependency(SomeType(), id: UUID().uuidString)
91 | }
92 | }
93 | ```
94 |
95 | ### 3.3 Unique IDs for States and Dependencies
96 |
97 | When no ID is provided, AppState generates a default ID based on the location in the source code. This ensures that each `State` or `Dependency` is unique and protected from unintended access.
98 |
99 | ### Example
100 |
101 | ```swift
102 | extension Application {
103 | var defaultState: State {
104 | state(initial: 0) // AppState generates a unique ID
105 | }
106 |
107 | var defaultDependency: Dependency {
108 | dependency(SomeType()) // AppState generates a unique ID
109 | }
110 | }
111 | ```
112 |
113 | ### 3.4 File-Private State and Dependency Access
114 |
115 | For even more restricted access within the same Swift file, use the `fileprivate` access level to protect states and dependencies from being accessed externally.
116 |
117 | ### Example
118 |
119 | ```swift
120 | fileprivate extension Application {
121 | var fileprivateState: State {
122 | state(initial: 0)
123 | }
124 |
125 | var fileprivateDependency: Dependency {
126 | dependency(SomeType())
127 | }
128 | }
129 | ```
130 |
131 | ### 3.5 Understanding AppState's Storage Mechanism
132 |
133 | AppState uses a unified cache to store `State`, `Dependency`, `StoredState`, and `SyncState`. This ensures that these data types are efficiently managed across your app.
134 |
135 | By default, AppState assigns a name value as "App", which ensures that all values associated with a module are tied to that name. This makes it harder to access these states and dependencies from other modules.
136 |
137 | ## 4. AppState vs SwiftUI's Environment
138 |
139 | AppState and SwiftUI's Environment both offer ways to manage shared state and dependencies in your application, but they differ in scope, functionality, and use cases.
140 |
141 | ### 4.1 SwiftUI's Environment
142 |
143 | SwiftUI’s Environment is a built-in mechanism that allows you to pass shared data down through a view hierarchy. It’s ideal for passing data that many views need access to, but it has limitations when it comes to more complex state management.
144 |
145 | **Strengths:**
146 | - Simple to use and well integrated with SwiftUI.
147 | - Ideal for lightweight data that needs to be shared across multiple views in a hierarchy.
148 |
149 | **Limitations:**
150 | - Data is only available within the specific view hierarchy. Accessing the same data across different view hierarchies is not possible without additional work.
151 | - Less control over thread safety and persistence compared to AppState.
152 | - Lack of built-in persistence or synchronization mechanisms.
153 |
154 | ### 4.2 AppState
155 |
156 | AppState provides a more powerful and flexible system for managing state across the entire application, with thread safety, persistence, and dependency injection capabilities.
157 |
158 | **Strengths:**
159 | - Centralized state management, accessible across the entire app, not just in specific view hierarchies.
160 | - Built-in persistence mechanisms (`StoredState`, `FileState`, and `SyncState`).
161 | - Type safety and thread safety guarantees, ensuring that state is accessed and modified correctly.
162 | - Can handle more complex state and dependency management.
163 |
164 | **Limitations:**
165 | - Requires more setup and configuration compared to SwiftUI's Environment.
166 | - Somewhat less integrated with SwiftUI compared to Environment, though still works well in SwiftUI apps.
167 |
168 | ### 4.3 When to Use Each
169 |
170 | - Use **SwiftUI's Environment** when you have simple data that needs to be shared across a view hierarchy, like user settings or theming preferences.
171 | - Use **AppState** when you need centralized state management, persistence, or more complex state that needs to be accessed across the entire app.
172 |
173 | ## Conclusion
174 |
175 | By using these advanced techniques, such as just-in-time creation, preloading, state and dependency management, and understanding the differences between AppState and SwiftUI's Environment, you can build efficient and resource-conscious applications with **AppState**.
176 |
--------------------------------------------------------------------------------
/documentation/best-practices.md:
--------------------------------------------------------------------------------
1 | # Best Practices for Using AppState
2 |
3 | This guide provides best practices to help you use AppState efficiently and effectively in your Swift applications.
4 |
5 | ## 1. Use AppState Sparingly
6 |
7 | AppState is versatile and suitable for both shared and localized state management. It's ideal for data that needs to be shared across multiple components, persist across views or user sessions, or be managed at the component level. However, overuse can lead to unnecessary complexity.
8 |
9 | ### Recommendation:
10 | - Use AppState for critical application-wide data and shared state, but avoid using it for small, localized data that doesn't need to persist or be accessed across different components.
11 |
12 | ## 2. Maintain a Clean AppState
13 |
14 | As your application expands, your AppState might grow in complexity. Regularly review and refactor your AppState to remove unused states and dependencies. Keeping your AppState clean makes it simpler to understand, maintain, and test.
15 |
16 | ### Recommendation:
17 | - Periodically audit your AppState for unused or redundant states and dependencies.
18 | - Refactor large AppState structures to keep them clean and manageable.
19 |
20 | ## 3. Test Your AppState
21 |
22 | Like other aspects of your application, ensure that your AppState is thoroughly tested. Use mock dependencies to isolate your AppState from external dependencies during testing, and confirm that each part of your application behaves as expected.
23 |
24 | ### Recommendation:
25 | - Use XCTest or similar frameworks to test AppState behavior and interactions.
26 | - Mock or stub dependencies to ensure AppState tests are isolated and reliable.
27 |
28 | ## 4. Use the Slice Feature Wisely
29 |
30 | The `Slice` feature allows you to access specific parts of an AppState’s state, which is useful for handling large and complex state structures. However, use this feature wisely to maintain a clean and well-organized AppState, avoiding unnecessary slices that fragment state handling.
31 |
32 | ### Recommendation:
33 | - Only use `Slice` for large or nested states where accessing individual components is necessary.
34 | - Avoid over-slicing state, which can lead to confusion and fragmented state management.
35 |
36 | ## 5. Use Constants for Static Values
37 |
38 | The `@Constant` feature lets you define read-only constants that can be shared across your application. It’s useful for values that remain unchanged throughout your app’s lifecycle, like configuration settings or predefined data. Constants ensure that these values are not modified unintentionally.
39 |
40 | ### Recommendation:
41 | - Use `@Constant` for values that remain unchanged, such as app configurations, environment variables, or static references.
42 |
43 | ## 6. Modularize Your AppState
44 |
45 | For larger applications, consider breaking your AppState into smaller, more manageable modules. Each module can have its own state and dependencies, which are then composed into the overall AppState. This can make your AppState easier to understand, test, and maintain.
46 |
47 | ### Recommendation:
48 | - Divide AppState into logical modules to manage state and dependencies at a more granular level.
49 | - Compose modules into the main AppState to maintain modularity and separation of concerns.
50 |
51 | ## 7. Leverage Just-In-Time Creation
52 |
53 | AppState values are created just in time, meaning they are instantiated only when accessed. This optimizes memory usage and ensures that AppState values are only created when necessary.
54 |
55 | ### Recommendation:
56 | - Allow AppState values to be created just-in-time rather than preloading all states and dependencies unnecessarily.
57 |
58 | ## Conclusion
59 |
60 | Every application is unique, so these best practices may not fit every situation. Always consider your application's specific requirements when deciding how to use AppState, and strive to keep your state management clean, efficient, and well-tested.
61 |
--------------------------------------------------------------------------------
/documentation/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing to AppState
2 |
3 | Thank you for considering contributing to **AppState**! Your contributions help make this project better for everyone.
4 |
5 | ## How to Contribute
6 |
7 | ### 1. Reporting Bugs
8 |
9 | If you encounter any bugs, please open an issue on GitHub. When reporting a bug, please include:
10 |
11 | - A clear and descriptive title.
12 | - A detailed description of the bug, including steps to reproduce it.
13 | - The expected behavior and what actually happened.
14 | - The version of **AppState** you are using.
15 | - Any relevant screenshots or logs.
16 |
17 | ### 2. Suggesting Features
18 |
19 | New ideas are welcome! If you have a feature you'd like to see added to **AppState**, please open an issue and describe:
20 |
21 | - The problem the feature would solve.
22 | - How you think the feature should work.
23 | - Any additional context or examples that would help illustrate your idea.
24 |
25 | ### 3. Submitting Pull Requests
26 |
27 | If you'd like to contribute code to **AppState**, follow these steps:
28 |
29 | 1. **Fork the Repository**: Create a personal fork of the **AppState** repository on GitHub.
30 | 2. **Clone Your Fork**: Clone your fork to your local machine:
31 | ```bash
32 | git clone https://github.com/your-username/AppState.git
33 | ```
34 | 3. **Create a New Branch**: Create a new branch for your feature or bugfix:
35 | ```bash
36 | git checkout -b my-feature-branch
37 | ```
38 | 4. **Make Changes**: Implement your changes in the new branch.
39 | 5. **Test Your Changes**: Ensure your changes pass all tests. Add new tests if necessary.
40 | 6. **Commit Your Changes**: Commit your changes with a descriptive commit message:
41 | ```bash
42 | git commit -m "Add my new feature"
43 | ```
44 | 7. **Push to GitHub**: Push your branch to your GitHub fork:
45 | ```bash
46 | git push origin my-feature-branch
47 | ```
48 | 8. **Create a Pull Request**: Go to the **AppState** repository on GitHub and create a pull request from your branch.
49 |
50 | ### 4. Code Style
51 |
52 | Please follow the coding style guidelines used in the **AppState** project. Consistent code style helps make the codebase more maintainable and easier to review.
53 |
54 | ### 5. License
55 |
56 | By contributing to **AppState**, you agree that your contributions will be licensed under the same license as the project: [LICENSE](https://github.com/0xLeif/AppState/blob/main/LICENSE).
57 |
58 | ## Thank You!
59 |
60 | Your contributions are highly valued and appreciated. Thank you for helping improve **AppState**!
61 |
--------------------------------------------------------------------------------
/documentation/installation.md:
--------------------------------------------------------------------------------
1 | # Installation Guide
2 |
3 | This guide will walk you through the process of installing **AppState** into your Swift project using Swift Package Manager.
4 |
5 | ## Swift Package Manager
6 |
7 | **AppState** can be easily integrated into your project using Swift Package Manager. Follow the steps below to add **AppState** as a dependency.
8 |
9 | ### Step 1: Update Your `Package.swift` File
10 |
11 | Add **AppState** to the `dependencies` section of your `Package.swift` file:
12 |
13 | ```swift
14 | dependencies: [
15 | .package(url: "https://github.com/0xLeif/AppState.git", from: "2.0.0")
16 | ]
17 | ```
18 |
19 | ### Step 2: Add AppState to Your Target
20 |
21 | Include AppState in your target’s dependencies:
22 |
23 | ```swift
24 | .target(
25 | name: "YourTarget",
26 | dependencies: ["AppState"]
27 | )
28 | ```
29 |
30 | ### Step 3: Build Your Project
31 |
32 | Once you’ve added AppState to your `Package.swift` file, build your project to fetch the dependency and integrate it into your codebase.
33 |
34 | ```
35 | swift build
36 | ```
37 |
38 | ### Step 4: Import AppState in Your Code
39 |
40 | Now, you can start using AppState in your project by importing it at the top of your Swift files:
41 |
42 | ```swift
43 | import AppState
44 | ```
45 |
46 | ## Xcode
47 |
48 | If you prefer to add **AppState** directly through Xcode, follow these steps:
49 |
50 | ### Step 1: Open Your Xcode Project
51 |
52 | Open your Xcode project or workspace.
53 |
54 | ### Step 2: Add a Swift Package Dependency
55 |
56 | 1. Navigate to the project navigator and select your project file.
57 | 2. In the project editor, select your target, and then go to the "Swift Packages" tab.
58 | 3. Click the "+" button to add a package dependency.
59 |
60 | ### Step 3: Enter the Repository URL
61 |
62 | In the "Choose Package Repository" dialog, enter the following URL: `https://github.com/0xLeif/AppState.git`
63 |
64 | Then click "Next."
65 |
66 | ### Step 4: Specify the Version
67 |
68 | Choose the version you wish to use. It's recommended to select the "Up to Next Major Version" option and specify `2.0.0` as the lower bound. Then click "Next."
69 |
70 | ### Step 5: Add the Package
71 |
72 | Xcode will fetch the package and present you with options to add **AppState** to your target. Make sure to select the correct target and click "Finish."
73 |
74 | ### Step 6: Import `AppState` in Your Code
75 |
76 | You can now import **AppState** at the top of your Swift files:
77 |
78 | ```swift
79 | import AppState
80 | ```
81 |
82 | ## Next Steps
83 |
84 | With AppState installed, you can move on to the [Usage Overview](usage-overview.md) to see how to implement the key features in your project.
85 |
--------------------------------------------------------------------------------
/documentation/migration-considerations.md:
--------------------------------------------------------------------------------
1 |
2 | # Migration Considerations
3 |
4 | When updating your data model, particularly for persisted or synchronized data, you need to handle backward compatibility to avoid potential issues when older data is loaded. Here are a few important points to keep in mind:
5 |
6 | ## 1. Adding Non-Optional Fields
7 | If you add new non-optional fields to your model, decoding old data (which won't contain those fields) may fail. To avoid this:
8 | - Consider giving new fields default values.
9 | - Make the new fields optional to ensure compatibility with older versions of your app.
10 |
11 | ### Example:
12 | ```swift
13 | struct Settings: Codable {
14 | var text: String
15 | var isDarkMode: Bool
16 | var newField: String? // New field is optional
17 | }
18 | ```
19 |
20 | ## 2. Data Format Changes
21 | If you modify the structure of a model (e.g., changing a type from `Int` to `String`), the decoding process might fail when reading older data. Plan for a smooth migration by:
22 | - Creating migration logic to convert old data formats to the new structure.
23 | - Using `Decodable`'s custom initializer to handle old data and map it to your new model.
24 |
25 | ### Example:
26 | ```swift
27 | struct Settings: Codable {
28 | var text: String
29 | var isDarkMode: Bool
30 | var version: Int
31 |
32 | // Custom decoding logic for older versions
33 | init(from decoder: Decoder) throws {
34 | let container = try decoder.container(keyedBy: CodingKeys.self)
35 | self.text = try container.decode(String.self, forKey: .text)
36 | self.isDarkMode = try container.decode(Bool.self, forKey: .isDarkMode)
37 | self.version = (try? container.decode(Int.self, forKey: .version)) ?? 1 // Default for older data
38 | }
39 | }
40 | ```
41 |
42 | ## 3. Handling Deleted or Deprecated Fields
43 | If you remove a field from the model, ensure that old versions of the app can still decode the new data without crashing. You can:
44 | - Ignore extra fields when decoding.
45 | - Use custom decoders to handle older data and manage deprecated fields properly.
46 |
47 | ## 4. Versioning Your Models
48 |
49 | Versioning your models allows you to handle changes in your data structure over time. By keeping a version number as part of your model, you can easily implement migration logic to convert older data formats to newer ones. This approach ensures that your app can handle older data structures while smoothly transitioning to new versions.
50 |
51 | - **Why Versioning is Important**: When users update their app, they may still have older data persisted on their devices. Versioning helps your app recognize the data's format and apply the correct migration logic.
52 | - **How to Use**: Add a `version` field to your model and check it during the decoding process to determine if migration is needed.
53 |
54 | ### Example:
55 | ```swift
56 | struct Settings: Codable {
57 | var version: Int
58 | var text: String
59 | var isDarkMode: Bool
60 |
61 | // Handle version-specific decoding logic
62 | init(from decoder: Decoder) throws {
63 | let container = try decoder.container(keyedBy: CodingKeys.self)
64 | self.version = try container.decode(Int.self, forKey: .version)
65 | self.text = try container.decode(String.self, forKey: .text)
66 | self.isDarkMode = try container.decode(Bool.self, forKey: .isDarkMode)
67 |
68 | // If migrating from an older version, apply necessary transformations here
69 | if version < 2 {
70 | // Migrate older data to newer format
71 | }
72 | }
73 | }
74 | ```
75 |
76 | - **Best Practice**: Start with a `version` field from the beginning. Each time you update your model structure, increment the version and handle the necessary migration logic.
77 |
78 | ## 5. Testing Migration
79 | Always test your migration thoroughly by simulating loading old data with new versions of your model to ensure your app behaves as expected.
80 |
--------------------------------------------------------------------------------
/documentation/syncstate-implementation.md:
--------------------------------------------------------------------------------
1 | # SyncState Implementation in AppState
2 |
3 | This guide covers how to set up and configure SyncState in your application, including setting up iCloud capabilities and understanding potential limitations.
4 |
5 | ## 1. Setting Up iCloud Capabilities
6 |
7 | To use SyncState in your application, you first need to enable iCloud in your project and configure Key-Value storage.
8 |
9 | ### Steps to Enable iCloud and Key-Value Storage:
10 |
11 | 1. Open your Xcode project and navigate to your project settings.
12 | 2. Under the "Signing & Capabilities" tab, select your target (iOS or macOS).
13 | 3. Click the "+ Capability" button and choose "iCloud" from the list.
14 | 4. Enable the "Key-Value storage" option under iCloud settings. This allows your app to store and sync small amounts of data using iCloud.
15 |
16 | ### Entitlements File Configuration:
17 |
18 | 1. In your Xcode project, find or create the **entitlements file** for your app.
19 | 2. Ensure that the iCloud Key-Value Store is correctly set up in the entitlements file with the correct iCloud container.
20 |
21 | Example in the entitlements file:
22 |
23 | ```xml
24 | com.apple.developer.ubiquity-kvstore-identifier
25 | $(TeamIdentifierPrefix)com.yourdomain.app
26 | ```
27 |
28 | Make sure that the string value matches the iCloud container associated with your project.
29 |
30 | ## 2. Using SyncState in Your Application
31 |
32 | Once iCloud is enabled, you can use `SyncState` in your application to synchronize data across devices.
33 |
34 | ### Example of SyncState in Use:
35 |
36 | ```swift
37 | import AppState
38 | import SwiftUI
39 |
40 | extension Application {
41 | var syncValue: SyncState {
42 | syncState(id: "syncValue")
43 | }
44 | }
45 |
46 | struct ContentView: View {
47 | @SyncState(\.syncValue) private var syncValue: Int?
48 |
49 | var body: some View {
50 | VStack {
51 | if let syncValue = syncValue {
52 | Text("SyncValue: \(syncValue)")
53 | } else {
54 | Text("No SyncValue")
55 | }
56 |
57 | Button("Update SyncValue") {
58 | syncValue = Int.random(in: 0..<100)
59 | }
60 | }
61 | }
62 | }
63 | ```
64 |
65 | In this example, the sync state will be saved to iCloud and synchronized across devices logged into the same iCloud account.
66 |
67 | ## 3. Limitations and Best Practices
68 |
69 | SyncState uses `NSUbiquitousKeyValueStore`, which has some limitations:
70 |
71 | - **Storage Limit**: SyncState is designed for small amounts of data. The total storage limit is 1 MB, and each key-value pair is limited to around 1 MB.
72 | - **Synchronization**: Changes made to the SyncState are not instantly synchronized across devices. There can be a slight delay in synchronization, and iCloud syncing may occasionally be affected by network conditions.
73 |
74 | ### Best Practices:
75 |
76 | - **Use SyncState for Small Data**: Ensure that only small data like user preferences or settings are synchronized using SyncState.
77 | - **Handle SyncState Failures Gracefully**: Use default values or error handling mechanisms to account for potential sync delays or failures.
78 |
79 | ## 4. Conclusion
80 |
81 | By properly configuring iCloud and understanding the limitations of SyncState, you can leverage its power to sync data across devices. Make sure you only use SyncState for small, critical pieces of data to avoid potential issues with iCloud storage limits.
82 |
--------------------------------------------------------------------------------
/documentation/usage-constant.md:
--------------------------------------------------------------------------------
1 | # Constant Usage
2 |
3 | `Constant` in the **AppState** library provides read-only access to values within your application's state. It works similarly to `Slice`, but ensures that the accessed values are immutable. This makes `Constant` ideal for accessing values that may otherwise be mutable but should remain read-only in certain contexts.
4 |
5 | ## Key Features
6 |
7 | - **Read-Only Access**: Constants provide access to mutable state, but the values cannot be modified.
8 | - **Scoped to Application**: Like `Slice`, `Constant` is defined within the `Application` extension and scoped to access specific parts of the state.
9 | - **Thread-Safe**: `Constant` ensures safe access to state in concurrent environments.
10 |
11 | ## Example Usage
12 |
13 | ### Defining a Constant in Application
14 |
15 | Here’s how you define a `Constant` in the `Application` extension to access a read-only value:
16 |
17 | ```swift
18 | import AppState
19 | import SwiftUI
20 |
21 | struct ExampleValue {
22 | var username: String?
23 | var isLoading: Bool
24 | let value: String
25 | var mutableValue: String
26 | }
27 |
28 | extension Application {
29 | var exampleValue: State {
30 | state(
31 | initial: ExampleValue(
32 | username: "Leif",
33 | isLoading: false,
34 | value: "value",
35 | mutableValue: ""
36 | )
37 | )
38 | }
39 | }
40 | ```
41 |
42 | ### Accessing the Constant in a SwiftUI View
43 |
44 | In a SwiftUI view, you can use the `@Constant` property wrapper to access the constant state in a read-only manner:
45 |
46 | ```swift
47 | import AppState
48 | import SwiftUI
49 |
50 | struct ExampleView: View {
51 | @Constant(\.exampleValue, \.value) var constantValue: String
52 |
53 | var body: some View {
54 | Text("Constant Value: \(constantValue)")
55 | }
56 | }
57 | ```
58 |
59 | ### Read-Only Access to Mutable State
60 |
61 | Even if the value is mutable elsewhere, when accessed through `@Constant`, the value becomes immutable:
62 |
63 | ```swift
64 | import AppState
65 | import SwiftUI
66 |
67 | struct ExampleView: View {
68 | @Constant(\.exampleValue, \.mutableValue) var constantMutableValue: String
69 |
70 | var body: some View {
71 | Text("Read-Only Mutable Value: \(constantMutableValue)")
72 | }
73 | }
74 | ```
75 |
76 | ## Best Practices
77 |
78 | - **Use for Read-Only Access**: Use `Constant` to access parts of the state that should not be modified within certain contexts, even if they are mutable elsewhere.
79 | - **Thread-Safe**: Like other AppState components, `Constant` ensures thread-safe access to state.
80 | - **Use `OptionalConstant` for Optional Values**: If the part of the state you're accessing may be `nil`, use `OptionalConstant` to safely handle the absence of a value.
81 |
82 | ## Conclusion
83 |
84 | `Constant` and `OptionalConstant` provide an efficient way to access specific parts of your app's state in a read-only manner. They ensure that values which may otherwise be mutable are treated as immutable when accessed within a view, ensuring safety and clarity in your code.
85 |
--------------------------------------------------------------------------------
/documentation/usage-filestate.md:
--------------------------------------------------------------------------------
1 | # FileState Usage
2 |
3 | `FileState` is a component of the **AppState** library that allows you to store and retrieve persistent data using the file system. It is useful for storing large data or complex objects that need to be saved between app launches and restored when needed.
4 |
5 | ## Key Features
6 |
7 | - **Persistent Storage**: Data stored using `FileState` persists across app launches.
8 | - **Large Data Handling**: Unlike `StoredState`, `FileState` is ideal for handling larger or more complex data.
9 | - **Thread-Safe**: Like other AppState components, `FileState` ensures safe access to the data in concurrent environments.
10 |
11 | ## Example Usage
12 |
13 | ### Storing and Retrieving Data with FileState
14 |
15 | Here's how to define a `FileState` in the `Application` extension to store and retrieve a large object:
16 |
17 | ```swift
18 | import AppState
19 | import SwiftUI
20 |
21 | struct UserProfile: Codable {
22 | var name: String
23 | var age: Int
24 | }
25 |
26 | extension Application {
27 | @MainActor
28 | var userProfile: FileState {
29 | fileState(initial: UserProfile(name: "Guest", age: 25), filename: "userProfile")
30 | }
31 | }
32 |
33 | struct FileStateExampleView: View {
34 | @FileState(\.userProfile) var userProfile: UserProfile
35 |
36 | var body: some View {
37 | VStack {
38 | Text("Name: \(userProfile.name), Age: \(userProfile.age)")
39 | Button("Update Profile") {
40 | userProfile = UserProfile(name: "UpdatedName", age: 30)
41 | }
42 | }
43 | }
44 | }
45 | ```
46 |
47 | ### Handling Large Data with FileState
48 |
49 | When you need to handle larger datasets or objects, `FileState` ensures the data is stored efficiently in the app's file system. This is useful for scenarios like caching or offline storage.
50 |
51 | ```swift
52 | import AppState
53 | import SwiftUI
54 |
55 | extension Application {
56 | @MainActor
57 | var largeDataset: FileState<[String]> {
58 | fileState(initial: [], filename: "largeDataset")
59 | }
60 | }
61 |
62 | struct LargeDataView: View {
63 | @FileState(\.largeDataset) var largeDataset: [String]
64 |
65 | var body: some View {
66 | List(largeDataset, id: \.self) { item in
67 | Text(item)
68 | }
69 | }
70 | }
71 | ```
72 |
73 | ### Migration Considerations
74 |
75 | When updating your data model, it's important to account for potential migration challenges, especially when working with persisted data using **StoredState**, **FileState**, or **SyncState**. Without proper migration handling, changes like adding new fields or modifying data formats can cause issues when older data is loaded.
76 |
77 | Here are some key points to keep in mind:
78 | - **Adding New Non-Optional Fields**: Ensure new fields are either optional or have default values to maintain backward compatibility.
79 | - **Handling Data Format Changes**: If the structure of your model changes, implement custom decoding logic to support old formats.
80 | - **Versioning Your Models**: Use a `version` field in your models to help with migrations and apply logic based on the data’s version.
81 |
82 | To learn more about how to manage migrations and avoid potential issues, refer to the [Migration Considerations Guide](migration-considerations.md).
83 |
84 |
85 | ## Best Practices
86 |
87 | - **Use for Large or Complex Data**: If you're storing large data or complex objects, `FileState` is ideal over `StoredState`.
88 | - **Thread-Safe Access**: Like other components of **AppState**, `FileState` ensures data is accessed safely even when multiple tasks interact with the stored data.
89 | - **Combine with Codable**: When working with custom data types, ensure they conform to `Codable` to simplify encoding and decoding to and from the file system.
90 |
91 | ## Conclusion
92 |
93 | `FileState` is a powerful tool for handling persistent data in your app, allowing you to store and retrieve larger or more complex objects in a thread-safe and persistent manner. It works seamlessly with Swift’s `Codable` protocol, ensuring your data can be easily serialized and deserialized for long-term storage.
94 |
--------------------------------------------------------------------------------
/documentation/usage-observeddependency.md:
--------------------------------------------------------------------------------
1 | # ObservedDependency Usage
2 |
3 | `ObservedDependency` is a component of the **AppState** library that allows you to use dependencies that conform to `ObservableObject`. This is useful when you want the dependency to notify your SwiftUI views about changes, making your views reactive and dynamic.
4 |
5 | ## Key Features
6 |
7 | - **Observable Dependencies**: Use dependencies that conform to `ObservableObject`, allowing the dependency to automatically update your views when its state changes.
8 | - **Reactive UI Updates**: SwiftUI views automatically update when changes are published by the observed dependency.
9 | - **Thread-Safe**: Like other AppState components, `ObservedDependency` ensures thread-safe access to the observed dependency.
10 |
11 | ## Example Usage
12 |
13 | ### Defining an Observable Dependency
14 |
15 | Here's how to define an observable service as a dependency in the `Application` extension:
16 |
17 | ```swift
18 | import AppState
19 | import SwiftUI
20 |
21 | @MainActor
22 | class ObservableService: ObservableObject {
23 | @Published var count: Int = 0
24 | }
25 |
26 | extension Application {
27 | @MainActor
28 | var observableService: Dependency {
29 | dependency(ObservableService())
30 | }
31 | }
32 | ```
33 |
34 | ### Using the Observed Dependency in a SwiftUI View
35 |
36 | In your SwiftUI view, you can access the observable dependency using the `@ObservedDependency` property wrapper. The observed object automatically updates the view whenever its state changes.
37 |
38 | ```swift
39 | import AppState
40 | import SwiftUI
41 |
42 | struct ObservedDependencyExampleView: View {
43 | @ObservedDependency(\.observableService) var service: ObservableService
44 |
45 | var body: some View {
46 | VStack {
47 | Text("Count: \(service.count)")
48 | Button("Increment Count") {
49 | service.count += 1
50 | }
51 | }
52 | }
53 | }
54 | ```
55 |
56 | ### Test Case
57 |
58 | The following test case demonstrates the interaction with `ObservedDependency`:
59 |
60 | ```swift
61 | import XCTest
62 | @testable import AppState
63 |
64 | @MainActor
65 | fileprivate class ObservableService: ObservableObject {
66 | @Published var count: Int
67 |
68 | init() {
69 | count = 0
70 | }
71 | }
72 |
73 | fileprivate extension Application {
74 | @MainActor
75 | var observableService: Dependency {
76 | dependency(ObservableService())
77 | }
78 | }
79 |
80 | @MainActor
81 | fileprivate struct ExampleDependencyWrapper {
82 | @ObservedDependency(\.observableService) var service
83 |
84 | func test() {
85 | service.count += 1
86 | }
87 | }
88 |
89 | final class ObservedDependencyTests: XCTestCase {
90 | @MainActor
91 | func testDependency() async {
92 | let example = ExampleDependencyWrapper()
93 |
94 | XCTAssertEqual(example.service.count, 0)
95 |
96 | example.test()
97 |
98 | XCTAssertEqual(example.service.count, 1)
99 | }
100 | }
101 | ```
102 |
103 | ### Reactive UI Updates
104 |
105 | Since the dependency conforms to `ObservableObject`, any changes to its state will trigger a UI update in the SwiftUI view. You can bind the state directly to UI elements like a `Picker`:
106 |
107 | ```swift
108 | import AppState
109 | import SwiftUI
110 |
111 | struct ReactiveView: View {
112 | @ObservedDependency(\.observableService) var service: ObservableService
113 |
114 | var body: some View {
115 | Picker("Select Count", selection: $service.count) {
116 | ForEach(0..<10) { count in
117 | Text("\(count)").tag(count)
118 | }
119 | }
120 | }
121 | }
122 | ```
123 |
124 | ## Best Practices
125 |
126 | - **Use for Observable Services**: `ObservedDependency` is ideal when your dependency needs to notify views of changes, especially for services that provide data or state updates.
127 | - **Leverage Published Properties**: Ensure your dependency uses `@Published` properties to trigger updates in your SwiftUI views.
128 | - **Thread-Safe**: Like other AppState components, `ObservedDependency` ensures thread-safe access and modifications to the observable service.
129 |
130 | ## Conclusion
131 |
132 | `ObservedDependency` is a powerful tool for managing observable dependencies within your app. By leveraging Swift's `ObservableObject` protocol, it ensures that your SwiftUI views remain reactive and up-to-date with changes in the service or resource.
133 |
--------------------------------------------------------------------------------
/documentation/usage-overview.md:
--------------------------------------------------------------------------------
1 | # Usage Overview
2 |
3 | This overview provides a quick introduction to using the key components of the **AppState** library within a SwiftUI `View`. Each section includes simple examples that fit into the scope of a SwiftUI view structure.
4 |
5 | ## Defining Values in Application Extension
6 |
7 | To define application-wide state or dependencies, you should extend the `Application` object. This allows you to centralize all your app’s state in one place. Here's an example of how to extend `Application` to create various states and dependencies:
8 |
9 | ```swift
10 | import AppState
11 |
12 | extension Application {
13 | var user: State {
14 | state(initial: User(name: "Guest", isLoggedIn: false))
15 | }
16 |
17 | var userPreferences: StoredState {
18 | storedState(initial: "Default Preferences", id: "userPreferences")
19 | }
20 |
21 | var darkModeEnabled: SyncState {
22 | syncState(initial: false, id: "darkModeEnabled")
23 | }
24 |
25 | var userToken: SecureState {
26 | secureState(id: "userToken")
27 | }
28 |
29 | @MainActor
30 | var largeDataset: FileState<[String]> {
31 | fileState(initial: [], filename: "largeDataset")
32 | }
33 | }
34 | ```
35 |
36 | ## State
37 |
38 | `State` allows you to define application-wide state that can be accessed and modified anywhere in your app.
39 |
40 | ### Example
41 |
42 | ```swift
43 | import AppState
44 | import SwiftUI
45 |
46 | struct ContentView: View {
47 | @AppState(\.user) var user: User
48 |
49 | var body: some View {
50 | VStack {
51 | Text("Hello, \(user.name)!")
52 | Button("Log in") {
53 | user.isLoggedIn.toggle()
54 | }
55 | }
56 | }
57 | }
58 | ```
59 |
60 | ## StoredState
61 |
62 | `StoredState` persists state using `UserDefaults` to ensure that values are saved across app launches.
63 |
64 | ### Example
65 |
66 | ```swift
67 | import AppState
68 | import SwiftUI
69 |
70 | struct PreferencesView: View {
71 | @StoredState(\.userPreferences) var userPreferences: String
72 |
73 | var body: some View {
74 | VStack {
75 | Text("Preferences: \(userPreferences)")
76 | Button("Update Preferences") {
77 | userPreferences = "Updated Preferences"
78 | }
79 | }
80 | }
81 | }
82 | ```
83 |
84 | ## SyncState
85 |
86 | `SyncState` synchronizes app state across multiple devices using iCloud.
87 |
88 | ### Example
89 |
90 | ```swift
91 | import AppState
92 | import SwiftUI
93 |
94 | struct SyncSettingsView: View {
95 | @SyncState(\.darkModeEnabled) var isDarkModeEnabled: Bool
96 |
97 | var body: some View {
98 | VStack {
99 | Toggle("Dark Mode", isOn: $isDarkModeEnabled)
100 | }
101 | }
102 | }
103 | ```
104 |
105 | ## FileState
106 |
107 | `FileState` is used to store larger or more complex data persistently using the file system, making it ideal for caching or saving data that doesn't fit within the limitations of `UserDefaults`.
108 |
109 | ### Example
110 |
111 | ```swift
112 | import AppState
113 | import SwiftUI
114 |
115 | struct LargeDataView: View {
116 | @FileState(\.largeDataset) var largeDataset: [String]
117 |
118 | var body: some View {
119 | List(largeDataset, id: \.self) { item in
120 | Text(item)
121 | }
122 | }
123 | }
124 | ```
125 |
126 | ## SecureState
127 |
128 | `SecureState` stores sensitive data securely in the Keychain.
129 |
130 | ### Example
131 |
132 | ```swift
133 | import AppState
134 | import SwiftUI
135 |
136 | struct SecureView: View {
137 | @SecureState(\.userToken) var userToken: String?
138 |
139 | var body: some View {
140 | VStack {
141 | if let token = userToken {
142 | Text("User token: \(token)")
143 | } else {
144 | Text("No token found.")
145 | }
146 | Button("Set Token") {
147 | userToken = "secure_token_value"
148 | }
149 | }
150 | }
151 | }
152 | ```
153 |
154 | ## Constant
155 |
156 | `Constant` provides immutable, read-only access to values within your application’s state, ensuring safety when accessing values that should not be modified.
157 |
158 | ### Example
159 |
160 | ```swift
161 | import AppState
162 | import SwiftUI
163 |
164 | struct ExampleView: View {
165 | @Constant(\.user, \.name) var name: String
166 |
167 | var body: some View {
168 | Text("Username: \(name)")
169 | }
170 | }
171 | ```
172 |
173 | ## Slicing State
174 |
175 | `Slice` and `OptionalSlice` allow you to access specific parts of your application’s state.
176 |
177 | ### Example
178 |
179 | ```swift
180 | import AppState
181 | import SwiftUI
182 |
183 | struct SlicingView: View {
184 | @Slice(\.user, \.name) var name: String
185 |
186 | var body: some View {
187 | VStack {
188 | Text("Username: \(name)")
189 | Button("Update Username") {
190 | name = "NewUsername"
191 | }
192 | }
193 | }
194 | }
195 | ```
196 |
197 | ## Best Practices
198 |
199 | - **Use `AppState` in SwiftUI Views**: Property wrappers like `@AppState`, `@StoredState`, `@FileState`, `@SecureState`, and others are designed to be used within the scope of SwiftUI views.
200 | - **Define State in Application Extension**: Centralize state management by extending `Application` to define your app’s state and dependencies.
201 | - **Reactive Updates**: SwiftUI automatically updates views when state changes, so you don’t need to manually refresh the UI.
202 | - **[Best Practices Guide](best-practices.md)**: For a detailed breakdown of best practices when using AppState.
203 |
204 | ## Next Steps
205 |
206 | After familiarizing yourself with the basic usage, you can explore more advanced topics:
207 |
208 | - Explore using **FileState** for persisting large amounts of data to files in the [FileState Usage Guide](usage-filestate.md).
209 | - Learn about **Constants** and how to use them for immutable values in your app's state in the [Constant Usage Guide](usage-constant.md).
210 | - Investigate how **Dependency** is used in AppState to handle shared services, and see examples in the [State Dependency Usage Guide](usage-state-dependency.md).
211 | - Delve deeper into **Advanced SwiftUI** techniques like using `ObservedDependency` for managing observable dependencies in views in the [ObservedDependency Usage Guide](usage-observeddependency.md).
212 | - For more advanced usage techniques, like Just-In-Time creation and preloading dependencies, see the [Advanced Usage Guide](advanced-usage.md).
213 |
--------------------------------------------------------------------------------
/documentation/usage-securestate.md:
--------------------------------------------------------------------------------
1 | # SecureState Usage
2 |
3 | `SecureState` is a component of the **AppState** library that allows you to store sensitive data securely in the Keychain. It's best suited for storing small pieces of data like tokens or passwords that need to be securely encrypted.
4 |
5 | ## Key Features
6 |
7 | - **Secure Storage**: Data stored using `SecureState` is encrypted and securely saved in the Keychain.
8 | - **Persistence**: The data remains persistent across app launches, allowing secure retrieval of sensitive values.
9 |
10 | ## Keychain Limitations
11 |
12 | While `SecureState` is very secure, it has certain limitations:
13 |
14 | - **Limited Storage Size**: Keychain is designed for small pieces of data. It is not suitable for storing large files or datasets.
15 | - **Performance**: Accessing the Keychain is slower than accessing `UserDefaults`, so use it only when necessary to securely store sensitive data.
16 |
17 | ## Example Usage
18 |
19 | ### Storing a Secure Token
20 |
21 | ```swift
22 | import AppState
23 | import SwiftUI
24 |
25 | extension Application {
26 | var userToken: SecureState {
27 | secureState(id: "userToken")
28 | }
29 | }
30 |
31 | struct SecureView: View {
32 | @SecureState(\.userToken) var userToken: String?
33 |
34 | var body: some View {
35 | VStack {
36 | if let token = userToken {
37 | Text("User token: \(token)")
38 | } else {
39 | Text("No token found.")
40 | }
41 | Button("Set Token") {
42 | userToken = "secure_token_value"
43 | }
44 | }
45 | }
46 | }
47 | ```
48 |
49 | ### Handling Absence of Secure Data
50 |
51 | When accessing the Keychain for the first time, or if there’s no value stored, `SecureState` will return `nil`. Ensure you handle this scenario properly:
52 |
53 | ```swift
54 | if let token = userToken {
55 | print("Token: \(token)")
56 | } else {
57 | print("No token available.")
58 | }
59 | ```
60 |
61 | ## Best Practices
62 |
63 | - **Use for Small Data**: Keychain should be used for storing small pieces of sensitive information like tokens, passwords, and keys.
64 | - **Avoid Large Datasets**: If you need to store large datasets securely, consider using file-based encryption or other methods, as Keychain is not designed for large data storage.
65 | - **Handle nil**: Always handle cases where the Keychain returns `nil` when no value is present.
66 |
--------------------------------------------------------------------------------
/documentation/usage-slice.md:
--------------------------------------------------------------------------------
1 | # Slice and OptionalSlice Usage
2 |
3 | `Slice` and `OptionalSlice` are components of the **AppState** library that allow you to access specific parts of your application’s state. They are useful when you need to manipulate or observe a part of a more complex state structure.
4 |
5 | ## Overview
6 |
7 | - **Slice**: Allows you to access and modify a specific part of an existing `State` object.
8 | - **OptionalSlice**: Works similarly to `Slice` but is designed to handle optional values, such as when part of your state may or may not be `nil`.
9 |
10 | ### Key Features
11 |
12 | - **Selective State Access**: Access only the part of the state that you need.
13 | - **Thread Safety**: Just like with other state management types in **AppState**, `Slice` and `OptionalSlice` are thread-safe.
14 | - **Reactiveness**: SwiftUI views update when the slice of the state changes, ensuring your UI remains reactive.
15 |
16 | ## Example Usage
17 |
18 | ### Using Slice
19 |
20 | In this example, we use `Slice` to access and update a specific part of the state—in this case, the `username` from a more complex `User` object stored in the app state.
21 |
22 | ```swift
23 | import AppState
24 | import SwiftUI
25 |
26 | struct User {
27 | var username: String
28 | var email: String
29 | }
30 |
31 | extension Application {
32 | var user: State {
33 | state(initial: User(username: "Guest", email: "guest@example.com"))
34 | }
35 | }
36 |
37 | struct SlicingView: View {
38 | @Slice(\.user, \.username) var username: String
39 |
40 | var body: some View {
41 | VStack {
42 | Text("Username: \(username)")
43 | Button("Update Username") {
44 | username = "NewUsername"
45 | }
46 | }
47 | }
48 | }
49 | ```
50 |
51 | ### Using OptionalSlice
52 |
53 | `OptionalSlice` is useful when part of your state may be `nil`. In this example, the `User` object itself may be `nil`, so we use `OptionalSlice` to safely handle this case.
54 |
55 | ```swift
56 | import AppState
57 | import SwiftUI
58 |
59 | extension Application {
60 | var user: State {
61 | state(initial: nil)
62 | }
63 | }
64 |
65 | struct OptionalSlicingView: View {
66 | @OptionalSlice(\.user, \.username) var username: String?
67 |
68 | var body: some View {
69 | VStack {
70 | if let username = username {
71 | Text("Username: \(username)")
72 | } else {
73 | Text("No username available")
74 | }
75 | Button("Set Username") {
76 | username = "UpdatedUsername"
77 | }
78 | }
79 | }
80 | }
81 | ```
82 |
83 | ## Best Practices
84 |
85 | - **Use `Slice` for non-optional state**: If your state is guaranteed to be non-optional, use `Slice` to access and update it.
86 | - **Use `OptionalSlice` for optional state**: If your state or part of the state is optional, use `OptionalSlice` to handle cases where the value may be `nil`.
87 | - **Thread Safety**: Just like with `State`, `Slice` and `OptionalSlice` are thread-safe and designed to work with Swift’s concurrency model.
88 |
89 | ## Conclusion
90 |
91 | `Slice` and `OptionalSlice` provide powerful ways to access and modify specific parts of your state in a thread-safe manner. By leveraging these components, you can simplify state management in more complex applications, ensuring your UI stays reactive and up-to-date.
92 |
--------------------------------------------------------------------------------
/documentation/usage-state-dependency.md:
--------------------------------------------------------------------------------
1 | # State and Dependency Usage
2 |
3 | **AppState** provides powerful tools for managing application-wide state and injecting dependencies into SwiftUI views. By centralizing your state and dependencies, you can ensure your application remains consistent and maintainable.
4 |
5 | ## Overview
6 |
7 | - **State**: Represents a value that can be shared across the app. State values can be modified and observed within your SwiftUI views.
8 | - **Dependency**: Represents a shared resource or service that can be injected and accessed within SwiftUI views.
9 |
10 | ### Key Features
11 |
12 | - **Centralized State**: Define and manage application-wide state in one place.
13 | - **Dependency Injection**: Inject and access shared services and resources across different components of your application.
14 |
15 | ## Example Usage
16 |
17 | ### Defining Application State
18 |
19 | To define application-wide state, extend the `Application` object and declare the state properties.
20 |
21 | ```swift
22 | import AppState
23 |
24 | struct User {
25 | var name: String
26 | var isLoggedIn: Bool
27 | }
28 |
29 | extension Application {
30 | var user: State {
31 | state(initial: User(name: "Guest", isLoggedIn: false))
32 | }
33 | }
34 | ```
35 |
36 | ### Accessing and Modifying State in a View
37 |
38 | You can access and modify state values directly within a SwiftUI view using the `@AppState` property wrapper.
39 |
40 | ```swift
41 | import AppState
42 | import SwiftUI
43 |
44 | struct ContentView: View {
45 | @AppState(\.user) var user: User
46 |
47 | var body: some View {
48 | VStack {
49 | Text("Hello, \(user.name)!")
50 | Button("Log in") {
51 | user.name = "John Doe"
52 | user.isLoggedIn = true
53 | }
54 | }
55 | }
56 | }
57 | ```
58 |
59 | ### Defining Dependencies
60 |
61 | You can define shared resources, such as a network service, as dependencies in the `Application` object. These dependencies can be injected into SwiftUI views.
62 |
63 | ```swift
64 | import AppState
65 |
66 | protocol NetworkServiceType {
67 | func fetchData() -> String
68 | }
69 |
70 | class NetworkService: NetworkServiceType {
71 | func fetchData() -> String {
72 | return "Data from network"
73 | }
74 | }
75 |
76 | extension Application {
77 | var networkService: Dependency {
78 | dependency(NetworkService())
79 | }
80 | }
81 | ```
82 |
83 | ### Accessing Dependencies in a View
84 |
85 | Access dependencies within a SwiftUI view using the `@AppDependency` property wrapper. This allows you to inject services like a network service into your view.
86 |
87 | ```swift
88 | import AppState
89 | import SwiftUI
90 |
91 | struct NetworkView: View {
92 | @AppDependency(\.networkService) var networkService: NetworkServiceType
93 |
94 | var body: some View {
95 | VStack {
96 | Text("Data: \(networkService.fetchData())")
97 | }
98 | }
99 | }
100 | ```
101 |
102 | ### Combining State and Dependencies in a View
103 |
104 | State and dependencies can work together to build more complex application logic. For example, you can fetch data from a service and update the state:
105 |
106 | ```swift
107 | import AppState
108 | import SwiftUI
109 |
110 | struct CombinedView: View {
111 | @AppState(\.user) var user: User
112 | @AppDependency(\.networkService) var networkService: NetworkServiceType
113 |
114 | var body: some View {
115 | VStack {
116 | Text("User: \(user.name)")
117 | Button("Fetch Data") {
118 | user.name = networkService.fetchData()
119 | user.isLoggedIn = true
120 | }
121 | }
122 | }
123 | }
124 | ```
125 |
126 | ### Best Practices
127 |
128 | - **Centralize State**: Keep your application-wide state in one place to avoid duplication and ensure consistency.
129 | - **Use Dependencies for Shared Services**: Inject dependencies like network services, databases, or other shared resources to avoid tight coupling between components.
130 |
131 | ## Conclusion
132 |
133 | With **AppState**, you can manage application-wide state and inject shared dependencies directly into your SwiftUI views. This pattern helps keep your app modular and maintainable. Explore other features of the **AppState** library, such as [SecureState](usage-securestate.md) and [SyncState](usage-syncstate.md), to further enhance your app's state management.
134 |
--------------------------------------------------------------------------------
/documentation/usage-storedstate.md:
--------------------------------------------------------------------------------
1 | # StoredState Usage
2 |
3 | `StoredState` is a component of the **AppState** library that allows you to store and persist small amounts of data using `UserDefaults`. It is ideal for storing lightweight, non-sensitive data that should persist across app launches.
4 |
5 | ## Overview
6 |
7 | - **StoredState** is built on top of `UserDefaults`, which means it’s fast and efficient for storing small amounts of data (such as user preferences or app settings).
8 | - Data saved in **StoredState** persists across app sessions, allowing you to restore application state on launch.
9 |
10 | ### Key Features
11 |
12 | - **Persistent Storage**: Data saved in `StoredState` remains available between app launches.
13 | - **Small Data Handling**: Best used for lightweight data like preferences, toggles, or small configurations.
14 | - **Thread-Safe**: `StoredState` ensures that data access remains safe in concurrent environments.
15 |
16 | ## Example Usage
17 |
18 | ### Defining a StoredState
19 |
20 | You can define a **StoredState** by extending the `Application` object and declaring the state property:
21 |
22 | ```swift
23 | import AppState
24 |
25 | extension Application {
26 | var userPreferences: StoredState {
27 | storedState(initial: "Default Preferences", id: "userPreferences")
28 | }
29 | }
30 | ```
31 |
32 | ### Accessing and Modifying StoredState in a View
33 |
34 | You can access and modify **StoredState** values within SwiftUI views using the `@StoredState` property wrapper:
35 |
36 | ```swift
37 | import AppState
38 | import SwiftUI
39 |
40 | struct PreferencesView: View {
41 | @StoredState(\.userPreferences) var userPreferences: String
42 |
43 | var body: some View {
44 | VStack {
45 | Text("Preferences: \(userPreferences)")
46 | Button("Update Preferences") {
47 | userPreferences = "Updated Preferences"
48 | }
49 | }
50 | }
51 | }
52 | ```
53 |
54 | ## Handling Data Migration
55 |
56 | As your app evolves, you may update the models that are persisted via **StoredState**. When updating your data model, ensure backward compatibility. For example, you might add new fields or version your model to handle migration.
57 |
58 | For more information, refer to the [Migration Considerations Guide](migration-considerations.md).
59 |
60 | ### Migration Considerations
61 |
62 | - **Adding New Non-Optional Fields**: Ensure new fields are either optional or have default values to maintain backward compatibility.
63 | - **Versioning Models**: If your data model changes over time, include a `version` field to manage different versions of your persisted data.
64 |
65 | ## Best Practices
66 |
67 | - **Use for Small Data**: Store lightweight, non-sensitive data that needs to persist across app launches, like user preferences.
68 | - **Consider Alternatives for Larger Data**: If you need to store large amounts of data, consider using **FileState** instead.
69 |
70 | ## Conclusion
71 |
72 | **StoredState** is a simple and efficient way to persist small pieces of data using `UserDefaults`. It is ideal for saving preferences and other small settings across app launches while providing safe access and easy integration with SwiftUI. For more complex persistence needs, explore other **AppState** features like [FileState](usage-filestate.md) or [SyncState](usage-syncstate.md).
73 |
--------------------------------------------------------------------------------
/documentation/usage-syncstate.md:
--------------------------------------------------------------------------------
1 | # SyncState Usage
2 |
3 | `SyncState` is a component of the **AppState** library that allows you to synchronize app state across multiple devices using iCloud. This is especially useful for keeping user preferences, settings, or other important data consistent across devices.
4 |
5 | ## Overview
6 |
7 | `SyncState` leverages iCloud’s `NSUbiquitousKeyValueStore` to keep small amounts of data in sync across devices. This makes it ideal for syncing lightweight application state such as preferences or user settings.
8 |
9 | ### Key Features
10 |
11 | - **iCloud Synchronization**: Automatically sync state across all devices logged into the same iCloud account.
12 | - **Persistent Storage**: Data is stored persistently in iCloud, meaning it will persist even if the app is terminated or restarted.
13 | - **Near Real-Time Sync**: Changes to the state are propagated to other devices almost instantly.
14 |
15 | > **Note**: `SyncState` is supported on watchOS 9.0 and later.
16 |
17 | ## Example Usage
18 |
19 | ### Data Model
20 |
21 | Assume we have a struct named `Settings` that conforms to `Codable`:
22 |
23 | ```swift
24 | struct Settings: Codable {
25 | var text: String
26 | var isShowingSheet: Bool
27 | var isDarkMode: Bool
28 | }
29 | ```
30 |
31 | ### Defining a SyncState
32 |
33 | You can define a `SyncState` by extending the `Application` object and declaring the state properties that should be synced:
34 |
35 | ```swift
36 | extension Application {
37 | var settings: SyncState {
38 | syncState(
39 | initial: Settings(
40 | text: "Hello, World!",
41 | isShowingSheet: false,
42 | isDarkMode: false
43 | ),
44 | id: "settings"
45 | )
46 | }
47 | }
48 | ```
49 |
50 | ### Handling External Changes
51 |
52 | To ensure the app responds to external changes from iCloud, override the `didChangeExternally` function by creating a custom `Application` subclass:
53 |
54 | ```swift
55 | class CustomApplication: Application {
56 | override func didChangeExternally(notification: Notification) {
57 | super.didChangeExternally(notification: notification)
58 |
59 | DispatchQueue.main.async {
60 | self.objectWillChange.send()
61 | }
62 | }
63 | }
64 | ```
65 |
66 | ### Creating Views to Modify and Sync State
67 |
68 | In the following example, we have two views: `ContentView` and `ContentViewInnerView`. These views share and sync the `Settings` state between them. `ContentView` allows the user to modify the `text` and toggle `isDarkMode`, while `ContentViewInnerView` displays the same text and updates it when tapped.
69 |
70 | ```swift
71 | struct ContentView: View {
72 | @SyncState(\.settings) private var settings: Settings
73 |
74 | var body: some View {
75 | VStack {
76 | TextField("", text: $settings.text)
77 |
78 | Button(settings.isDarkMode ? "Light" : "Dark") {
79 | settings.isDarkMode.toggle()
80 | }
81 |
82 | Button("Show") { settings.isShowingSheet = true }
83 | }
84 | .preferredColorScheme(settings.isDarkMode ? .dark : .light)
85 | .sheet(isPresented: $settings.isShowingSheet, content: ContentViewInnerView.init)
86 | }
87 | }
88 |
89 | struct ContentViewInnerView: View {
90 | @Slice(\.settings, \.text) private var text: String
91 |
92 | var body: some View {
93 | Text("\(text)")
94 | .onTapGesture {
95 | text = Date().formatted()
96 | }
97 | }
98 | }
99 | ```
100 |
101 | ### Setting Up the App
102 |
103 | Finally, set up the application in the `@main` struct. In the initialization, promote the custom application, enable logging, and load the iCloud store dependency for syncing:
104 |
105 | ```swift
106 | @main
107 | struct SyncStateExampleApp: App {
108 | init() {
109 | Application
110 | .promote(to: CustomApplication.self)
111 | .logging(isEnabled: true)
112 | .load(dependency: \.icloudStore)
113 | }
114 |
115 | var body: some Scene {
116 | WindowGroup {
117 | ContentView()
118 | }
119 | }
120 | }
121 | ```
122 |
123 | ### Enabling iCloud Key-Value Store
124 |
125 | To enable iCloud syncing, make sure you follow this guide to enable the iCloud Key-Value Store capability: [Starting to use SyncState](https://github.com/0xLeif/AppState/wiki/Starting-to-use-SyncState).
126 |
127 | ### SyncState: Notes on iCloud Storage
128 |
129 | While `SyncState` allows easy synchronization, it's important to remember the limitations of `NSUbiquitousKeyValueStore`:
130 |
131 | - **Storage Limit**: You can store up to 1 MB of data in iCloud using `NSUbiquitousKeyValueStore`, with a per-key value size limit of 1 MB.
132 |
133 | ### Migration Considerations
134 |
135 | When updating your data model, it's important to account for potential migration challenges, especially when working with persisted data using **StoredState**, **FileState**, or **SyncState**. Without proper migration handling, changes like adding new fields or modifying data formats can cause issues when older data is loaded.
136 |
137 | Here are some key points to keep in mind:
138 | - **Adding New Non-Optional Fields**: Ensure new fields are either optional or have default values to maintain backward compatibility.
139 | - **Handling Data Format Changes**: If the structure of your model changes, implement custom decoding logic to support old formats.
140 | - **Versioning Your Models**: Use a `version` field in your models to help with migrations and apply logic based on the data’s version.
141 |
142 | To learn more about how to manage migrations and avoid potential issues, refer to the [Migration Considerations Guide](migration-considerations.md).
143 |
144 | ## SyncState Implementation Guide
145 |
146 | For detailed instructions on how to configure iCloud and set up SyncState in your project, see the [SyncState Implementation Guide](syncstate-implementation.md).
147 |
148 | ## Best Practices
149 |
150 | - **Use for Small, Critical Data**: `SyncState` is ideal for synchronizing small, important pieces of state such as user preferences, settings, or feature flags.
151 | - **Monitor iCloud Storage**: Ensure that your usage of `SyncState` stays within iCloud storage limits to prevent data sync issues.
152 | - **Handle External Updates**: If your app needs to respond to state changes initiated on another device, override the `didChangeExternally` function to update the app's state in real time.
153 |
154 | ## Conclusion
155 |
156 | `SyncState` provides a powerful way to synchronize small amounts of application state across devices via iCloud. It is ideal for ensuring that user preferences and other key data remain consistent across all devices logged into the same iCloud account. For more advanced use cases, explore other features of **AppState**, such as [SecureState](usage-securestate.md) and [FileState](usage-filestate.md).
157 |
--------------------------------------------------------------------------------