AppState is a Swift Package that simplifies the management of application state in a thread-safe, type-safe, and SwiftUI-friendly way. Featuring dedicated struct types for managing state, AppState provides easy and coordinated access to this state across your application. Added to this, the package incorporates built-in logging mechanisms to aid debugging and error tracking. The AppState package also boasts a cache-based system to persistently store and retrieve any application-wide data at any given time.
Requirements: iOS 15.0+ / watchOS 8.0+ / macOS 11.0+ / tvOS 15.0+ / visionOS 1.0+ | Swift 5.9+ / Xcode 15+
Non Apple Platform Support: Linux & Windows
(🍎 Apple OS only)
- Application: Centralized class for all application-wide data with built-in observability for reactive changes.
- State: Struct for encapsulating and broadcasting value changes.
- StoredState: Struct for encapsulating and broadcasting stored value changes, using
UserDefaults
. - FileState: Struct for encapsulating and broadcasting stored value changes, using
FileManager
. - 🍎 SyncState: Struct for encapsulating and broadcasting stored value changes, using
iCloud
. - 🍎 SecureState: Struct for securely encapsulating and broadcasting stored value changes, using the device's Keychain.
- Dependency: Struct for encapsulating dependencies within the app's scope.
- Scope: Represents a specific context within an app, defined by a unique name and ID.
- Slice: Struct that provides access to and modification of specific AppState's state parts.
- OptionalSlice: Struct that provides access to and modification of specific AppState's state parts. Useful if the state value is optional.
- DependencySlice: Struct that provides access to and modification of specific AppState's dependency parts.
- AppState: Bridges
Application.State
withSwiftUI
. - StoredState: Stores its values to
UserDefaults
. - FileState: Stores its values using
FileManager
. - 🍎 SyncState: Stores its values to
iCloud
. - Slice: Allows users to access and modify specific AppState's state parts.
- OptionalSlice: Allows users to access and modify specific AppState's state parts. Useful if the state value is optional.
- Constant: Allows users to access a specific part of AppState's state.
- OptionalConstant: Allows users to access a specific part of AppState's state. Useful if the state value is optional.
- 🍎 SecureState: Securely stores its string values using the Keychain.
- AppDependency: Simplifies the handling of dependencies throughout your application.
- 🍎 ObservedDependency: Simplifies the handling of dependencies throughout your application. Dependencies must conform to ObservableObject. Backed by an
@ObservedObject
to publish changes to SwiftUI views. - DependencySlice: Allows users to access and modify a specific part of AppState's dependency.
- DependencyConstant: Allows users to access a specific part of AppState's dependency.
To integrate AppState into your Swift project, you'll need to use the Swift Package Manager (SPM). SPM makes it easy to manage Swift package dependencies. Here's what you need to do:
- Add a package dependency to your
Package.swift
file:
dependencies: [
.package(url: "https://github.com/0xLeif/AppState.git", from: "1.0.0")
]
If you're working with an App project, open your project in Xcode. Navigate to File > Swift Packages > Add Package Dependency...
and enter https://github.com/0xLeif/AppState.git
.
- Next, don't forget to add AppState as a target to your project. This step is necessary for both Xcode and SPM Package.swift.
After successfully adding AppState as a dependency, you need to import AppState into your Swift file where you want to use it. Here's a code example:
import AppState
Dependency Injection
import AppState
extension Application {
var networkService: Dependency<NetworkServiceType> {
dependency(NetworkService())
}
}
struct ContentView: View {
@AppDependency(\.networkService) var networkService
// Your view code
}
Keychain Storage
import AppState
extension Application {
var userToken: SecureState {
secureState(id: "userToken")
}
}
@SecureState(\.userToken) var userToken: String?
iCloud Synced State
import AppState
extension Application {
var isDarkModeEnabled: SyncState<Bool> {
syceState(inital: false, id: "isDarkModeEnabled")
}
}
@SyncState(\.isDarkModeEnabled) var isDarkModeEnabled: Bool
Slicing
import AppState
struct Preferences: Codable {
var isDarkModeEnabled: Bool
}
struct User {
let id: UUID
var username: String
var metadata: [String: Any]
}
extension Application {
// We can use iCloud to sync the preferences across the user's devices
var preferences: SyncState<Preferences> {
syncState(
initial: Preferences(
isDarkModeEnabled: false
),
id: "preferences"
)
}
// We can have an optional user state to signify if the user is logged in or not.
var user: State<User?> {
state(initial: nil)
}
var dictionary: State<[String: String]> {
state(initial: [:])
}
}
class ViewModel: ObservableObject {
// State that isn't optional can use `Slice` and `Constant`. `Slice` values are mutable.
@Slice(\.preferences, \.isDarkModeEnabled) var isDarkModeEnabled: Bool
// If the state is optional though, you must use the `OptionalSlice` or `OptionalConstant`.
// These values might be optional because of the root state, or the value might be optional too.
// If the root state is `nil` you can't update the values.
@OptionalConstant(\.user, \.id) var id: UUID?
@OptionalSlice(\.user, \.username) var username: String?
// You can slice into a Dictionary for a specific entry.
@OptionalSlice(\.user, \.metadata["lastLogin"]) var lastLogin: Any?
@Slice(\.dictionary, \.["error"]) var error: String?
}
AppState is designed to uphold application state management ease and intuitiveness. Here's how to use it:
Defining an application-wide state requires extending the Application
and declaring the variables that retain the state. Each state corresponds to an instance of the generic Application.State
struct:
extension Application {
var isLoading: State<Bool> {
state(initial: false)
}
var username: State<String> {
state(initial: "Leif")
}
var colors: State<[String: CGColor]> {
state(initial: ["primary": CGColor(red: 1, green: 0, blue: 1, alpha: 1)])
}
}
Once you define the state, it is straightforward to read and write it within your application:
var usernameState: Application.State = Application.state(\.username)
// Read the value
print(usernameState.value) // Output: "Leif"
// Modify the value
usernameState.value = "0xL"
print(Application.state(\.username).value) // Output: "0xL"
The AppState
property wrapper can directly bridge State of an Application
to SwiftUI:
struct ContentView: View {
@AppState(\.username) var username
var body: some View {
Button(
action: { username = "Hello!" }.
label: { Text("Hello, \(username)!") }
)
}
}
You can also use AppState
in a SwiftUI ObservableObject
:
class UserSettings: ObservableObject {
@AppState(\.username) var username
func updateUsername(newUsername: String) {
username = newUsername
}
}
struct ContentView: View {
@ObservedObject private var settings = UserSettings()
var body: some View {
VStack {
Text("User name: \(settings.username)")
Button("Update Username") {
settings.updateUsername(newUsername: "NewUserName")
}
}
}
}
Dependency
is a feature provided by AppState, allowing you to define shared resources or services in your application.
To define a dependency, you should extend the Application
object. Here's an example of defining a networkService
dependency:
extension Application {
var networkService: Dependency<NetworkServiceType> {
dependency(NetworkService())
}
}
In this example, Dependency<NetworkServiceType>
represents a type safe container for NetworkService
.
Once you've defined a dependency, you can access it anywhere in your app:
let networkService = Application.dependency(\.networkService)
This approach allows you to work with dependencies in a type-safe manner, avoiding the need to manually handle errors related to incorrect types.
AppState provides the @AppDependency
property wrapper that simplifies access to dependencies. When you annotate a property with @AppDependency
, it fetches the appropriate dependency from the Application
object directly.
struct ContentView: View {
@AppDependency(\.networkService) var networkService
// Your view code
}
In this case, ContentView has access to the networkService dependency and can use it within its code.
When your dependency is an ObservableObject
, any changes to it will automatically update your SwiftUI views. Make sure your service conforms to the ObservableObject
protocol. To do this, you should not use the @AppDependency
property wrapper, but instead use the @ObservedDependency
property wrapper.
Here's an example:
class DataService: ObservableObject {
@Published var data: [String]
func fetchData() { ... }
}
extension Application {
var dataService: Dependency<DataService> {
dependency(DataService())
}
}
struct ContentView: View {
@ObservedDependency(\.dataService) private var dataService
var body: some View {
List(dataService.data, id: \.self) { item in
Text(item)
}
.onAppear {
dataService.fetchData()
}
}
}
In this example, whenever data in DataService
changes, SwiftUI automatically updates the ContentView
.
One of the great advantages of using Dependency
in AppState is the capability to replace dependencies with mock versions during testing. This is incredibly useful for isolating parts of your application for unit testing.
You can replace a dependency by calling the Application.override
function. This function returns a DependencyOverride
, you'll want to hold onto this token for as long as you want the mock dependency to be effective. When the token is deallocated, the dependency reverts back to its original condition.
Here's an example:
// Real network service
extension Application {
var networkService: Dependency<NetworkServiceType> {
dependency(NetworkService())
}
}
// Mock network service
class MockNetworkService: NetworkServiceType {
// Your mock implementation
}
func testNetworkService() {
// Keep hold of the `DependencyOverride` for the duration of your test.
let networkOverride = Application.override(\.networkService, with: MockNetworkService())
let mockNetworkService = Application.dependency(\.networkService)
// Once done, you can allow the `DependencyOverrideen` to be deallocated
// or call `networkOverride.cancel()` to revert back to the original service.
}
In AppState, you have the ability to promote your custom Application subclass to a shared singleton instance. This can be particularly useful when your Application subclass needs to conform to a protocol.
Here's an example of how to use the promote
function:
class CustomApplication: Application {
func customFunction() { ... }
}
Application.promote(to: CustomApplication.self)
By doing this, your custom Application subclass becomes the shared singleton instance that you can use throughout your application. This allows you to extend the functionalities of the Application class and utilize these extensions wherever you need in your application.
SyncState offers near real-time synchronization of application state across multiple devices using Apple's NSUbiquitousKeyValueStore. This allows for a consistent application state across various devices in your ecosystem. If your application operates on multiple platforms, SyncState ensures that all instances share the same state in near real-time.
NSUbiquitousKeyValueStore provides a lightweight, quick setup solution to store small amounts of data that are available ubiquitously across a user's multiple devices. The data is stored in iCloud and automatically syncs to all devices signed in to the same iCloud account, making it an ideal solution for synchronizing application state.
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.
For more information on synchronizing app preferences with iCloud, you can refer to Apple's official documentation.
AppState is released under the MIT License. See LICENSE for more information.
- If you found a bug, open an issue.
- If you have a feature request, open an issue.
- If you want to contribute, submit a pull request.