107 lines
3.0 KiB
Swift
Executable File
107 lines
3.0 KiB
Swift
Executable File
/// A property wrapper that tracks changes of a property.
|
|
///
|
|
/// This allows values to only publish changes if they have changed since the
|
|
/// last time they were recieved.
|
|
@propertyWrapper
|
|
public struct TrackedChanges<Value: Sendable>: Sendable {
|
|
|
|
/// The current value wrapped in a tracking state.
|
|
private var value: TrackingState
|
|
|
|
/// Used to check if a new value is equal to an old value.
|
|
private var isEqual: @Sendable (Value, Value) -> Bool
|
|
|
|
/// Access to the underlying property that we are wrapping.
|
|
public var wrappedValue: Value {
|
|
get { value.currentValue }
|
|
set {
|
|
// Check if the new value is equal to the old value.
|
|
guard !isEqual(newValue, value.currentValue) else { return }
|
|
// If it's not equal then set it, as well as set the tracking to `.needsProcessed`.
|
|
value = .needsProcessed(newValue)
|
|
}
|
|
}
|
|
|
|
/// Create a new property that tracks it's changes.
|
|
///
|
|
/// - Parameters:
|
|
/// - wrappedValue: The value that we are wrapping.
|
|
/// - needsProcessed: Whether this value needs processed (default = false).
|
|
/// - isEqual: Method to compare old values against new values.
|
|
public init(
|
|
wrappedValue: Value,
|
|
needsProcessed: Bool = false,
|
|
isEqual: @escaping @Sendable (Value, Value) -> Bool
|
|
) {
|
|
self.value = .init(wrappedValue, needsProcessed: needsProcessed)
|
|
self.isEqual = isEqual
|
|
}
|
|
|
|
fileprivate enum TrackingState {
|
|
case hasProcessed(Value)
|
|
case needsProcessed(Value)
|
|
|
|
init(_ value: Value, needsProcessed: Bool) {
|
|
if needsProcessed {
|
|
self = .needsProcessed(value)
|
|
} else {
|
|
self = .hasProcessed(value)
|
|
}
|
|
}
|
|
|
|
var currentValue: Value {
|
|
switch self {
|
|
case let .hasProcessed(value):
|
|
return value
|
|
case let .needsProcessed(value):
|
|
return value
|
|
}
|
|
}
|
|
|
|
var needsProcessed: Bool {
|
|
guard case .needsProcessed = self else { return false }
|
|
return true
|
|
}
|
|
}
|
|
|
|
/// Whether the value needs processed.
|
|
public var needsProcessed: Bool { value.needsProcessed }
|
|
|
|
public mutating func setHasProcessed() {
|
|
value = .hasProcessed(value.currentValue)
|
|
}
|
|
|
|
public var projectedValue: Self {
|
|
get { self }
|
|
set { self = newValue }
|
|
}
|
|
}
|
|
|
|
extension TrackedChanges: Equatable where Value: Equatable {
|
|
public static func == (lhs: TrackedChanges<Value>, rhs: TrackedChanges<Value>) -> Bool {
|
|
lhs.wrappedValue == rhs.wrappedValue
|
|
&& lhs.needsProcessed == rhs.needsProcessed
|
|
}
|
|
|
|
/// Create a new property that tracks it's changes, using the default equality check.
|
|
///
|
|
/// - Parameters:
|
|
/// - wrappedValue: The value that we are wrapping.
|
|
/// - needsProcessed: Whether this value needs processed (default = false).
|
|
public init(
|
|
wrappedValue: Value,
|
|
needsProcessed: Bool = false
|
|
) {
|
|
self.init(wrappedValue: wrappedValue, needsProcessed: needsProcessed) {
|
|
$0 == $1
|
|
}
|
|
}
|
|
}
|
|
|
|
extension TrackedChanges: Hashable where Value: Hashable {
|
|
|
|
public func hash(into hasher: inout Hasher) {
|
|
hasher.combine(wrappedValue)
|
|
}
|
|
}
|