Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add README and documentation #12

Merged
merged 1 commit into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,129 @@
# AppState

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.

## Key Features

- **Application:** Centralized class housing all application-wide data, equipped with built-in observability for reactive changes.

- **State:** Dedicated struct type for encapsulating and broadcasting value changes within the app's scope.

- **Scope:** Representation of a specific context within an app, defined by a unique name and ID.

- **AppState (property wrapper):** A property wrapper that elegantly bridges `Application.State` with `SwiftUI` for seamless integration.

### Requirements

- Swift 5.7 or later
- iOS 16.0 or later
- watchOS 9.0 or later
- macOS 13.0 or later
- tvOS 16.0 or later

## Getting Started

To add `AppState` to your Swift project, use the Swift Package Manager. This involves adding a package dependency to your `Package.swift` file.

```swift
dependencies: [
.package(url: "https://github.com/0xLeif/AppState.git", from: "0.1.0")
]
```

For App projects, open your project in Xcode and navigate to File > Swift Packages > Add Package Dependency... and enter `https://github.com/0xLeif/AppState.git`.

## Usage

AppState is designed to uphold application state management ease and intuitiveness. Here's how to use it:

### Define Application State

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:

```swift
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)])
}
}
```

### Read and Write Application States

Once you define the state, it is straightforward to read and write it within your application:

```swift
var appState: Application.State = Application.state(\.username)

// Read the value
print(appState.value) // Output: "Leif"

// Modify the value
appState.value = "0xL"

print(Application.state(\.username).value) // Output: "0xL"
```

### Using the AppState Property Wrapper

The `AppState` property wrapper can directly bridge State of an `Application` to SwiftUI:

```swift
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`:

```swift
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")
}
}
}
}
```

## License

AppState is released under the MIT License. See [LICENSE](https://github.com/0xLeif/AppState/blob/main/LICENSE) for more information.

## Communication and Contribution

- 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.

***

This README is a work in progress. If you found any inaccuracies or areas that require clarification, please don't hesitate to create a pull request with improvements!
11 changes: 11 additions & 0 deletions Sources/AppState/AppState.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import Combine
import SwiftUI

/// `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`.
@propertyWrapper public struct AppState<Value>: DynamicProperty {
/// Holds the singleton instance of `Application`.
@ObservedObject private var app: Application = Application.shared

/// Path for accessing `State` from Application.
private let keyPath: KeyPath<Application, Application.State<Value>>

/// Represents the current value of the `State`.
public var wrappedValue: Value {
get {
app.value(keyPath: keyPath).value
Expand All @@ -20,19 +24,26 @@ import SwiftUI
}
}

/// A binding to the `State`'s value, which can be used with SwiftUI views.
public var projectedValue: Binding<Value> {
Binding(
get: { wrappedValue },
set: { wrappedValue = $0 }
)
}

/**
Initializes the AppState with a `keyPath` for accessing `State` in Application.

- Parameter keyPath: The `KeyPath` for accessing `State` in Application.
*/
public init(
_ keyPath: KeyPath<Application, Application.State<Value>>
) {
self.keyPath = keyPath
}

/// 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
public static subscript<OuterSelf: ObservableObject>(
_enclosingInstance observed: OuterSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Value>,
Expand Down
9 changes: 9 additions & 0 deletions Sources/AppState/Application+Scope.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
extension Application {
/**
`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.

For example, it could be used to scope a state to a particular screen or user interaction flow.
*/
struct Scope {
/// The name of the scope context
let name: String

/// The specific id for this scope context
let id: String

/// Key computed property which builds a unique key for a given scope by combining `name` and `id` separated by "/"
var key: String {
"\(name)/\(id)"
}
Expand Down
14 changes: 13 additions & 1 deletion Sources/AppState/Application+State.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
extension Application {
/// `State` encapsulates the value within the application's scope and allows any changes to be propagated throughout the scoped area.
public struct State<Value> {
/// A private backing storage for the value.
private var _value: Value

/// The current state value.
public var value: Value {
get {
guard
guard
let value = shared.cache.get(
scope.key,
as: Value.self
Expand All @@ -21,8 +25,16 @@ extension Application {
}
}

/// The scope in which this state exists.
let scope: Scope

/**
Creates a new state within a given scope initialized with the provided value.

- Parameters:
- value: The initial value of the state
- scope: The scope in which the state exists
*/
init(
initial value: Value,
scope: Scope
Expand Down
8 changes: 8 additions & 0 deletions Sources/AppState/Application+internal.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
extension Application {
/// Generates a specific identifier string for given code context
///
/// - Parameters:
/// - fileID: The file identifier of the code, generally provided by `#fileID` directive.
/// - function: The function name of the code, generally provided by `#function` directive.
/// - line: The line number of the code, generally provided by `#line` directive.
/// - column: The column number of the code, generally provided by `#column` directive.
/// - Returns: A string representing the specific location and context of the code. The format is `<fileID>[<function>@<line>|<column>]`.
static func codeID(
fileID: StaticString,
function: StaticString,
Expand Down
88 changes: 58 additions & 30 deletions Sources/AppState/Application+public.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
public extension Application {
/// Provides a description of the current application state
static var description: String {
let state = shared.cache.allValues
.sorted { lhsKeyValue, rhsKeyValue in
Expand All @@ -15,36 +16,43 @@ public extension Application {
"""
}

static func state<Value>(_ keyPath: KeyPath<Application, Value>) -> Value {
shared.value(keyPath: keyPath)
}
/**
Retrieves a dependency for the provided `id`. If dependency is not present, it is created once using the provided closure.

func state<Value>(
initial: @autoclosure () -> Value,
- Parameters:
- object: The closure returning the dependency.
- feature: The name of the feature to which the dependency belongs, default is "App".
- id: The specific identifier for this dependency.
- Returns: The requested dependency of type `Value`.
*/
static func dependency<Value>(
_ object: @autoclosure () -> Value,
feature: String = "App",
id: String
) -> State<Value> {
) -> Value {
let scope = Scope(name: feature, id: id)
let key = scope.key

guard let value = cache.get(key, as: Value.self) else {
let value = initial()
return State(initial: value, scope: scope)
guard let value = shared.cache.get(key, as: Value.self) else {
let value = object()
shared.cache.set(value: value, forKey: key)
return value
}

return State(initial: value, scope: scope)
return value
}

func state<Value>(
initial: @autoclosure () -> Value,
// Overloaded version of `dependency(_:feature:id:)` function where id is generated from the code context.
static func dependency<Value>(
_ object: @autoclosure () -> Value,
_ fileID: StaticString = #fileID,
_ function: StaticString = #function,
_ line: Int = #line,
_ column: Int = #column
) -> State<Value> {
state(
initial: initial(),
id: Application.codeID(
) -> Value {
dependency(
object(),
id: codeID(
fileID: fileID,
function: function,
line: line,
Expand All @@ -53,38 +61,58 @@ public extension Application {
)
}

static func dependency<Value>(
_ object: @autoclosure () -> Value,
/**
Retrieves a state from Application instance using the provided keypath.

- Parameter keyPath: KeyPath of the state value to be fetched
- Returns: The requested state of type `Value`.
*/
static func state<Value>(_ keyPath: KeyPath<Application, Value>) -> Value {
shared.value(keyPath: keyPath)
}

/**
Retrieves a state for the provided `id`. If the state is not present, it initializes a new state with the `initial` value.

- Parameters:
- initial: The closure that returns initial state value.
- feature: The name of the feature to which the state belongs, default is "App".
- id: The specific identifier for this state.
- Returns: The state of type `Value`.
*/
func state<Value>(
initial: @autoclosure () -> Value,
feature: String = "App",
id: String
) -> Value {
) -> State<Value> {
let scope = Scope(name: feature, id: id)
let key = scope.key

guard let value = shared.cache.get(key, as: Value.self) else {
let value = object()
shared.cache.set(value: value, forKey: key)
return value
guard let value = cache.get(key, as: Value.self) else {
let value = initial()
return State(initial: value, scope: scope)
}

return value
return State(initial: value, scope: scope)
}

static func dependency<Value>(
_ object: @autoclosure () -> Value,
// Overloaded version of `state(initial:feature:id:)` function where id is generated from the code context.
func state<Value>(
initial: @autoclosure () -> Value,
_ fileID: StaticString = #fileID,
_ function: StaticString = #function,
_ line: Int = #line,
_ column: Int = #column
) -> Value {
dependency(
object(),
id: codeID(
) -> State<Value> {
state(
initial: initial(),
id: Application.codeID(
fileID: fileID,
function: function,
line: line,
column: column
)
)
}

}
Loading