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 onChange variants with no params and with 2 params #1

Merged
merged 3 commits into from
Jul 13, 2024
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct ExampleView: View {

var body: some View {
TextField("Text", text: $text)
.onChange(of: text, debounceTime: .seconds(2)) { newValue in
.onChange(of: text, debounceTime: .seconds(2)) { oldValue, newValue in
// Action executed each time 2 seconds pass since change of text property
}
.task(id: text, debounceTime: .milliseconds(250)) {
Expand Down
131 changes: 125 additions & 6 deletions Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,99 @@ extension View {
/// changes.
///
/// Each time the value changes before `debounceTime` passes, the previous action will be cancelled and the next
/// action will be scheduled to run after that time passes again. This mean that the action will only execute
/// action will be scheduled to run after that time passes again. This means that the action will only execute
/// after changes to the value stay unmodified for the specified `debounceTime`.
///
/// - Parameters:
/// - value: The value to check against when determining whether to run the closure.
/// - duration: The time to wait after each value change before running `action` closure.
/// - initial: Whether the action should be run (after debounce time) when this view initially appears.
/// - debounceTime: The time to wait after each value change before running `action` closure.
/// - action: A closure to run when the value changes.
/// - Returns: A view that fires an action after debounced time when the specified value changes.
@available(iOS 17, *)
@available(macOS 14.0, *)
@available(tvOS 17.0, *)
@available(watchOS 10.0, *)
@available(visionOS 1.0, *)
public func onChange<Value>(
of value: Value,
initial: Bool = false,
debounceTime: Duration,
_ action: @escaping () -> Void
) -> some View where Value: Equatable {
self.modifier(
DebouncedChangeNoParamViewModifier(
trigger: value,
initial: initial,
action: action,
debounceDuration: debounceTime
)
)
}

/// Adds a modifier for this view that fires an action only when a specified `debounceTime` elapses between value
/// changes.
///
/// Each time the value changes before `debounceTime` passes, the previous action will be cancelled and the next
/// action will be scheduled to run after that time passes again. This means that the action will only execute
/// after changes to the value stay unmodified for the specified `debounceTime`.
///
/// - Parameters:
/// - value: The value to check against when determining whether to run the closure.
/// - initial: Whether the action should be run (after debounce time) when this view initially appears.
/// - debounceTime: The time to wait after each value change before running `action` closure.
/// - action: A closure to run when the value changes.
/// - oldValue: The old value that failed the comparison check (or the
/// initial value when requested).
/// - newValue: The new value that failed the comparison check.
/// - Returns: A view that fires an action after debounced time when the specified value changes.
@available(iOS 17, *)
@available(macOS 14.0, *)
@available(tvOS 17.0, *)
@available(watchOS 10.0, *)
@available(visionOS 1.0, *)
public func onChange<Value>(
of value: Value,
initial: Bool = false,
debounceTime: Duration,
_ action: @escaping (_ oldValue: Value, _ newValue: Value) -> Void
) -> some View where Value: Equatable {
self.modifier(
DebouncedChange2ParamViewModifier(
trigger: value,
initial: initial,
action: action,
debounceDuration: debounceTime
)
)
}

/// Adds a modifier for this view that fires an action only when a specified `debounceTime` elapses between value
/// changes.
///
/// Each time the value changes before `debounceTime` passes, the previous action will be cancelled and the next
/// action will be scheduled to run after that time passes again. This means that the action will only execute
/// after changes to the value stay unmodified for the specified `debounceTime`.
///
/// - Parameters:
/// - value: The value to check against when determining whether to run the closure.
/// - debounceTime: The time to wait after each value change before running `action` closure.
/// - action: A closure to run when the value changes.
/// - Returns: A view that fires an action after debounced time when the specified value changes.
@available(iOS 16.0, *)
@available(macOS 13.0, *)
@available(tvOS 16.0, *)
@available(watchOS 9.0, *)
@available(iOS, deprecated: 17.0, message: "Use `onChange` with a two or zero parameter action closure instead")
@available(macOS, deprecated: 14.0, message: "Use `onChange` with a two or zero parameter action closure instead")
@available(tvOS, deprecated: 17.0, message: "Use `onChange` with a two or zero parameter action closure instead")
@available(watchOS, deprecated: 10.0, message: "Use `onChange` with a two or zero parameter action closure instead")
public func onChange<Value>(
of value: Value,
debounceTime: Duration,
perform action: @escaping (_ newValue: Value) -> Void
) -> some View where Value: Equatable {
self.modifier(DebouncedChangeViewModifier(trigger: value, action: action) {
self.modifier(DebouncedChange1ParamViewModifier(trigger: value, action: action) {
try await Task.sleep(for: debounceTime)
})
}
Expand All @@ -32,7 +107,7 @@ extension View {
/// `debounceTime` elapses between value changes.
///
/// Each time the value changes before `debounceTime` passes, the previous action will be cancelled and the next
/// action will be scheduled to run after that time passes again. This mean that the action will only execute
/// action will be scheduled to run after that time passes again. This means that the action will only execute
/// after changes to the value stay unmodified for the specified `debounceTime` in seconds.
///
/// - Parameters:
Expand All @@ -49,13 +124,13 @@ extension View {
debounceTime: TimeInterval,
perform action: @escaping (_ newValue: Value) -> Void
) -> some View where Value: Equatable {
self.modifier(DebouncedChangeViewModifier(trigger: value, action: action) {
self.modifier(DebouncedChange1ParamViewModifier(trigger: value, action: action) {
try await Task.sleep(seconds: debounceTime)
})
}
}

private struct DebouncedChangeViewModifier<Value>: ViewModifier where Value: Equatable {
private struct DebouncedChange1ParamViewModifier<Value>: ViewModifier where Value: Equatable {
let trigger: Value
let action: (Value) -> Void
let sleep: @Sendable () async throws -> Void
Expand All @@ -72,3 +147,47 @@ private struct DebouncedChangeViewModifier<Value>: ViewModifier where Value: Equ
}
}
}

@available(iOS 17, *)
@available(macOS 14.0, *)
@available(tvOS 17.0, *)
@available(watchOS 10.0, *)
@available(visionOS 1.0, *)
private struct DebouncedChangeNoParamViewModifier<Value>: ViewModifier where Value: Equatable {
let trigger: Value
let initial: Bool
let action: () -> Void
let debounceDuration: Duration

@State private var debouncedTask: Task<Void, Never>?

func body(content: Content) -> some View {
content.onChange(of: trigger, initial: initial) {
debouncedTask?.cancel()
debouncedTask = Task.delayed(duration: debounceDuration, operation: action)
}
}
}

@available(iOS 17, *)
@available(macOS 14.0, *)
@available(tvOS 17.0, *)
@available(watchOS 10.0, *)
@available(visionOS 1.0, *)
private struct DebouncedChange2ParamViewModifier<Value>: ViewModifier where Value: Equatable {
let trigger: Value
let initial: Bool
let action: (Value, Value) -> Void
let debounceDuration: Duration

@State private var debouncedTask: Task<Void, Never>?

func body(content: Content) -> some View {
content.onChange(of: trigger, initial: initial) { lhs, rhs in
debouncedTask?.cancel()
debouncedTask = Task.delayed(duration: debounceDuration) {
action(lhs, rhs)
}
}
}
}
17 changes: 17 additions & 0 deletions Sources/DebouncedOnChange/Task+Delayed.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,21 @@ extension Task {
static func sleep(seconds: TimeInterval) async throws where Success == Never, Failure == Never {
try await Task<Success, Failure>.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
}

@available(iOS 17, *)
@available(macOS 14.0, *)
@available(tvOS 17.0, *)
@available(watchOS 10.0, *)
@available(visionOS 1.0, *)
static func delayed(
duration: Duration,
operation: @escaping () async -> Void
) -> Self where Success == Void, Failure == Never {
Self {
do {
try await Task<Never, Never>.sleep(for: duration)
await operation()
} catch {}
}
}
}
Loading