Skip to content

Commit

Permalink
Add Slice (#56)
Browse files Browse the repository at this point in the history
* Add Slice

* Update documentation and add Constant

* Add extra test for Slice

* Rename MutableCachedApplicationValue

* Add Constant to README Property Wrappers
  • Loading branch information
0xLeif authored Jan 4, 2024
1 parent e38e094 commit c125c28
Show file tree
Hide file tree
Showing 11 changed files with 422 additions and 5 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ AppState is a Swift Package that simplifies the management of application state

- **SyncState:** Dedicated struct type for encapsulating and broadcasting stored value changes within the app's scope. Values are stored using `iCloud`. Requires iOS 15.0, watchOS 9.0, macOS 11.0, or tvOS 15.0.

- **Slice:** Dedicated struct has the ability to access and modify a specific part of an AppState's state, providing a more fine-grained control over the AppState.

- **SecureState:** Dedicated struct type for securely encapsulating and broadcasting stored value changes within the app's scope. Values are securely stored using the device's Keychain. SecureState values are never stored in the cache and are always retrieved directly from the Keychain.

- **Dependency:** Dedicated struct type for encapsulating dependencies within the app's scope.
Expand All @@ -24,6 +26,10 @@ AppState is a Swift Package that simplifies the management of application state

- **SyncState (property wrapper):** A property wrapper that stores its values to `iCloud`. Works the same as `AppState` otherwise. Requires iOS 15.0, watchOS 9.0, macOS 11.0, or tvOS 15.0.

- **Slice (property wrapper):** A property wrapper that allows users to access and modify a specific part of an AppState's state. This provides a more fine-grained control over the AppState and makes it easier to manage complex states.

- **Constant (property wrapper):** A property wrapper that allows users to access a specific part of an AppState's state. This provides a more fine-grained control over the AppState and makes it easier to manage complex states.

- **SecureState (property wrapper):** A property wrapper that securely stores its string values using the Keychain. Provides the same integration benefits as AppState.

- **AppDependency (property wrapper):** A property wrapper that simplifies the handling of dependencies throughout your application.
Expand Down
94 changes: 94 additions & 0 deletions Sources/AppState/Application/Application+public.swift
Original file line number Diff line number Diff line change
Expand Up @@ -648,3 +648,97 @@ public extension Application {
)
}
}

// MARK: Slice Functions

extension Application {
/**
This function creates a `Slice` of AppState that allows access to a specific part of the AppState's state. It provides granular control over the AppState.

- Parameters:
- stateKeyPath: A KeyPath pointing to the state in AppState that should be sliced.
- valueKeyPath: A KeyPath pointing to the specific part of the state that should be accessed.
- fileID: The identifier of the file.
- function: The name of the declaration.
- line: The line number on which it appears.
- column: The column number in which it begins.

- Returns: A Slice that allows access to a specific part of an AppState's state.
*/
public static func slice<SlicedState: MutableApplicationState, Value, SliceValue>(
_ stateKeyPath: KeyPath<Application, SlicedState>,
_ valueKeyPath: KeyPath<Value, SliceValue>,
_ fileID: StaticString = #fileID,
_ function: StaticString = #function,
_ line: Int = #line,
_ column: Int = #column
) -> Slice<SlicedState, Value, SliceValue, KeyPath<Value, SliceValue>> where SlicedState.Value == Value {
let slice = Slice(
stateKeyPath,
value: valueKeyPath
)

log(
debug: {
let stateKeyPathString = String(describing: stateKeyPath)
let valueTypeCharacterCount = String(describing: Value.self).count
var valueKeyPathString = String(describing: valueKeyPath)

valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)

return "🍕 Getting Slice \(stateKeyPathString)\(valueKeyPathString) -> \(slice.value)"
},
fileID: fileID,
function: function,
line: line,
column: column
)

return slice
}

/**
This function creates a `Slice` of AppState that allows access and modification to a specific part of the AppState's state. It provides granular control over the AppState.

- Parameters:
- stateKeyPath: A KeyPath pointing to the state in AppState that should be sliced.
- valueKeyPath: A WritableKeyPath pointing to the specific part of the state that should be accessed.
- fileID: The identifier of the file.
- function: The name of the declaration.
- line: The line number on which it appears.
- column: The column number in which it begins.

- Returns: A Slice that allows access and modification to a specific part of an AppState's state.
*/
public static func slice<SlicedState: MutableApplicationState, Value, SliceValue>(
_ stateKeyPath: KeyPath<Application, SlicedState>,
_ valueKeyPath: WritableKeyPath<Value, SliceValue>,
_ fileID: StaticString = #fileID,
_ function: StaticString = #function,
_ line: Int = #line,
_ column: Int = #column
) -> Slice<SlicedState, Value, SliceValue, WritableKeyPath<Value, SliceValue>> where SlicedState.Value == Value {
let slice = Slice(
stateKeyPath,
value: valueKeyPath
)

log(
debug: {
let stateKeyPathString = String(describing: stateKeyPath)
let valueTypeCharacterCount = String(describing: Value.self).count
var valueKeyPathString = String(describing: valueKeyPath)

valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)

return "🍕 Getting Slice \(stateKeyPathString)\(valueKeyPathString) -> \(slice.value)"
},
fileID: fileID,
function: function,
line: line,
column: column
)

return slice
}
}
23 changes: 21 additions & 2 deletions Sources/AppState/Application/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,40 @@ open class Application: NSObject, ObservableObject {
function: StaticString,
line: Int,
column: Int
) {
log(
debug: { message },
fileID: fileID,
function: function,
line: line,
column: column
)
}

/// Internal log function.
static func log(
debug message: () -> String,
fileID: StaticString,
function: StaticString,
line: Int,
column: Int
) {
guard isLoggingEnabled else { return }

let excludedFileIDs: [String] = [
"AppState/Application+StoredState.swift",
"AppState/Application+SyncState.swift",
"AppState/Application+SecureState.swift"
"AppState/Application+SecureState.swift",
"AppState/Application+Slice.swift"
]
let isFileIDValue: Bool = excludedFileIDs.contains(fileID.description) == false

guard isFileIDValue else { return }

let debugMessage = message()
let codeID = codeID(fileID: fileID, function: function, line: line, column: column)

logger.debug("\(message) (\(codeID))")
logger.debug("\(debugMessage) (\(codeID))")
}

/// Internal log function.
Expand Down
36 changes: 36 additions & 0 deletions Sources/AppState/Application/Types/Application+Slice.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
extension Application {
/// `Slice` allows access and modification to a specific part of an AppState's state. Supports `State`, `SyncState`, and `StorageState`.
public struct Slice<
SlicedState: MutableApplicationState,
Value,
SliceValue,
SliceKeyPath: KeyPath<Value, SliceValue>
> where SlicedState.Value == Value {
/// A private backing storage for the value.
private var state: SlicedState
private let keyPath: SliceKeyPath

init(
_ stateKeyPath: KeyPath<Application, SlicedState>,
value valueKeyPath: SliceKeyPath
) {
self.state = shared.value(keyPath: stateKeyPath)
self.keyPath = valueKeyPath
}
}
}

extension Application.Slice where SliceKeyPath == KeyPath<Value, SliceValue> {
/// The current state value.
public var value: SliceValue {
state.value[keyPath: keyPath]
}
}

extension Application.Slice where SliceKeyPath == WritableKeyPath<Value, SliceValue> {
/// The current state value.
public var value: SliceValue {
get { state.value[keyPath: keyPath] }
set { state.value[keyPath: keyPath] = newValue }
}
}
2 changes: 1 addition & 1 deletion Sources/AppState/Application/Types/Application+State.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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>: CustomStringConvertible {
public struct State<Value>: MutableApplicationState, CustomStringConvertible {
enum StateType {
case state
case stored
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ extension Application {
}

/// `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`.
public struct StoredState<Value> {
public struct StoredState<Value>: MutableApplicationState {
@AppDependency(\.userDefaults) private var userDefaults: UserDefaults

/// The initial value of the state.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ extension Application {

- 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.
*/
public struct SyncState<Value: Codable> {
public struct SyncState<Value: Codable>: MutableApplicationState {
@AppDependency(\.icloudStore) private var icloudStore: NSUbiquitousKeyValueStore

/// The initial value of the state.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
public protocol MutableApplicationState {
associatedtype Value

var value: Value { get set }
}
63 changes: 63 additions & 0 deletions Sources/AppState/PropertyWrappers/Constant.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Combine
import SwiftUI

/// A property wrapper that provides access to a specific part of the AppState's state.
@propertyWrapper public struct Constant<SlicedState: MutableApplicationState, Value, SliceValue> where SlicedState.Value == Value {
/// Holds the singleton instance of `Application`.
@ObservedObject private var app: Application = Application.shared

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

/// Path for accessing `SliceValue` from `Value`.
private let valueKeyPath: KeyPath<Value, SliceValue>

private let fileID: StaticString
private let function: StaticString
private let line: Int
private let column: Int
private let sliceKeyPath: String

/// Represents the current value of the `State`.
public var wrappedValue: SliceValue {
Application.slice(
stateKeyPath,
valueKeyPath,
fileID,
function,
line,
column
).value
}

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

- Parameters:
- stateKeyPath: A KeyPath that points to the state in AppState that should be sliced.
- valueKeyPath: A KeyPath that points to the specific part of the state that should be accessed.
*/
public init(
_ stateKeyPath: KeyPath<Application, SlicedState>,
_ valueKeyPath: KeyPath<Value, SliceValue>,
_ fileID: StaticString = #fileID,
_ function: StaticString = #function,
_ line: Int = #line,
_ column: Int = #column
) {
self.stateKeyPath = stateKeyPath
self.valueKeyPath = valueKeyPath
self.fileID = fileID
self.function = function
self.line = line
self.column = column

let stateKeyPathString = String(describing: stateKeyPath)
let valueTypeCharacterCount = String(describing: Value.self).count
var valueKeyPathString = String(describing: valueKeyPath)

valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)

self.sliceKeyPath = "\(stateKeyPathString)\(valueKeyPathString)"
}
}
104 changes: 104 additions & 0 deletions Sources/AppState/PropertyWrappers/Slice.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import Combine
import SwiftUI

/// A property wrapper that provides access to a specific part of the AppState's state.
@propertyWrapper public struct Slice<SlicedState: MutableApplicationState, Value, SliceValue>: DynamicProperty where SlicedState.Value == Value {
/// Holds the singleton instance of `Application`.
@ObservedObject private var app: Application = Application.shared

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

/// Path for accessing `SliceValue` from `Value`.
private let valueKeyPath: WritableKeyPath<Value, SliceValue>

private let fileID: StaticString
private let function: StaticString
private let line: Int
private let column: Int
private let sliceKeyPath: String

/// Represents the current value of the `State`.
public var wrappedValue: SliceValue {
get {
Application.slice(
stateKeyPath,
valueKeyPath,
fileID,
function,
line,
column
).value
}
nonmutating set {
Application.log(
debug: "🍕 Setting Slice \(sliceKeyPath) = \(newValue)",
fileID: fileID,
function: function,
line: line,
column: column
)

var state = app.value(keyPath: stateKeyPath)
state.value[keyPath: valueKeyPath] = newValue
}
}

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

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

- Parameters:
- stateKeyPath: A KeyPath that points to the state in AppState that should be sliced.
- valueKeyPath: A WritableKeyPath that points to the specific part of the state that should be accessed.
*/
public init(
_ stateKeyPath: KeyPath<Application, SlicedState>,
_ valueKeyPath: WritableKeyPath<Value, SliceValue>,
_ fileID: StaticString = #fileID,
_ function: StaticString = #function,
_ line: Int = #line,
_ column: Int = #column
) {
self.stateKeyPath = stateKeyPath
self.valueKeyPath = valueKeyPath
self.fileID = fileID
self.function = function
self.line = line
self.column = column

let stateKeyPathString = String(describing: stateKeyPath)
let valueTypeCharacterCount = String(describing: Value.self).count
var valueKeyPathString = String(describing: valueKeyPath)

valueKeyPathString.removeFirst(valueTypeCharacterCount + 1)

self.sliceKeyPath = "\(stateKeyPathString)\(valueKeyPathString)"
}

/// 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, SliceValue>,
storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self>
) -> SliceValue {
get {
observed[keyPath: storageKeyPath].wrappedValue
}
set {
guard
let publisher = observed.objectWillChange as? ObservableObjectPublisher
else { return }

publisher.send()
observed[keyPath: storageKeyPath].wrappedValue = newValue
}
}
}
Loading

0 comments on commit c125c28

Please sign in to comment.