├── .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 | [![macOS Build](https://img.shields.io/github/actions/workflow/status/0xLeif/AppState/macOS.yml?label=macOS&branch=main)](https://github.com/0xLeif/AppState/actions/workflows/macOS.yml) 4 | [![Ubuntu Build](https://img.shields.io/github/actions/workflow/status/0xLeif/AppState/ubuntu.yml?label=Ubuntu&branch=main)](https://github.com/0xLeif/AppState/actions/workflows/ubuntu.yml) 5 | [![Windows Build](https://img.shields.io/github/actions/workflow/status/0xLeif/AppState/windows.yml?label=Windows&branch=main)](https://github.com/0xLeif/AppState/actions/workflows/windows.yml) 6 | [![License](https://img.shields.io/github/license/0xLeif/AppState)](https://github.com/0xLeif/AppState/blob/main/LICENSE) 7 | [![Version](https://img.shields.io/github/v/release/0xLeif/AppState)](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 | --------------------------------------------------------------------------------