From 706c090dc51ac8700c1d13247dd0e6d5dd43d8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Fri, 18 Aug 2023 12:00:01 +0200 Subject: [PATCH 1/3] Add onChange variants with no params and with 2 params --- .../DebouncedChangeViewModifier.swift | 131 +++++++++++++++++- Sources/DebouncedOnChange/Task+Delayed.swift | 17 +++ 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift b/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift index 710fa83..05e1ff7 100644 --- a/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift +++ b/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift @@ -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(xrOS 1.0, *) + public func onChange( + 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(xrOS 1.0, *) + public func onChange( + 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( 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) }) } @@ -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: @@ -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: ViewModifier where Value: Equatable { +private struct DebouncedChange1ParamViewModifier: ViewModifier where Value: Equatable { let trigger: Value let action: (Value) -> Void let sleep: @Sendable () async throws -> Void @@ -72,3 +147,47 @@ private struct DebouncedChangeViewModifier: ViewModifier where Value: Equ } } } + +@available(iOS 17, *) +@available(macOS 14.0, *) +@available(tvOS 17.0, *) +@available(watchOS 10.0, *) +@available(xrOS 1.0, *) +private struct DebouncedChangeNoParamViewModifier: ViewModifier where Value: Equatable { + let trigger: Value + let initial: Bool + let action: () -> Void + let debounceDuration: Duration + + @State private var debouncedTask: Task? + + 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(xrOS 1.0, *) +private struct DebouncedChange2ParamViewModifier: ViewModifier where Value: Equatable { + let trigger: Value + let initial: Bool + let action: (Value, Value) -> Void + let debounceDuration: Duration + + @State private var debouncedTask: Task? + + func body(content: Content) -> some View { + content.onChange(of: trigger, initial: initial) { a, b in + debouncedTask?.cancel() + debouncedTask = Task.delayed(duration: debounceDuration) { + action(a, b) + } + } + } +} diff --git a/Sources/DebouncedOnChange/Task+Delayed.swift b/Sources/DebouncedOnChange/Task+Delayed.swift index 7e6c78b..4e29d27 100644 --- a/Sources/DebouncedOnChange/Task+Delayed.swift +++ b/Sources/DebouncedOnChange/Task+Delayed.swift @@ -27,4 +27,21 @@ extension Task { static func sleep(seconds: TimeInterval) async throws where Success == Never, Failure == Never { try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) } + + @available(iOS 17, *) + @available(macOS 14.0, *) + @available(tvOS 17.0, *) + @available(watchOS 10.0, *) + @available(xrOS 1.0, *) + static func delayed( + duration: Duration, + operation: @escaping () async -> Void + ) -> Self where Success == Void, Failure == Never { + Self { + do { + try await Task.sleep(for: duration) + await operation() + } catch {} + } + } } From 11a0c094953f95d503544ed913609d7d028ddbef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 13 Jul 2024 15:17:49 +0200 Subject: [PATCH 2/3] Change xrOS to visonOS --- .../DebouncedOnChange/DebouncedChangeViewModifier.swift | 8 ++++---- Sources/DebouncedOnChange/Task+Delayed.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift b/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift index 05e1ff7..c571d5c 100644 --- a/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift +++ b/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift @@ -19,7 +19,7 @@ extension View { @available(macOS 14.0, *) @available(tvOS 17.0, *) @available(watchOS 10.0, *) - @available(xrOS 1.0, *) + @available(visionOS 1.0, *) public func onChange( of value: Value, initial: Bool = false, @@ -56,7 +56,7 @@ extension View { @available(macOS 14.0, *) @available(tvOS 17.0, *) @available(watchOS 10.0, *) - @available(xrOS 1.0, *) + @available(visionOS 1.0, *) public func onChange( of value: Value, initial: Bool = false, @@ -152,7 +152,7 @@ private struct DebouncedChange1ParamViewModifier: ViewModifier where Valu @available(macOS 14.0, *) @available(tvOS 17.0, *) @available(watchOS 10.0, *) -@available(xrOS 1.0, *) +@available(visionOS 1.0, *) private struct DebouncedChangeNoParamViewModifier: ViewModifier where Value: Equatable { let trigger: Value let initial: Bool @@ -173,7 +173,7 @@ private struct DebouncedChangeNoParamViewModifier: ViewModifier where Val @available(macOS 14.0, *) @available(tvOS 17.0, *) @available(watchOS 10.0, *) -@available(xrOS 1.0, *) +@available(visionOS 1.0, *) private struct DebouncedChange2ParamViewModifier: ViewModifier where Value: Equatable { let trigger: Value let initial: Bool diff --git a/Sources/DebouncedOnChange/Task+Delayed.swift b/Sources/DebouncedOnChange/Task+Delayed.swift index 4e29d27..cd169e6 100644 --- a/Sources/DebouncedOnChange/Task+Delayed.swift +++ b/Sources/DebouncedOnChange/Task+Delayed.swift @@ -32,7 +32,7 @@ extension Task { @available(macOS 14.0, *) @available(tvOS 17.0, *) @available(watchOS 10.0, *) - @available(xrOS 1.0, *) + @available(visionOS 1.0, *) static func delayed( duration: Duration, operation: @escaping () async -> Void From 8809ce7a7906d5f286ee41d97f44d22137e119cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 13 Jul 2024 15:31:42 +0200 Subject: [PATCH 3/3] Lint --- README.md | 2 +- .../DebouncedChangeViewModifier.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 151cdb3..7edb6e2 100644 --- a/README.md +++ b/README.md @@ -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)) { diff --git a/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift b/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift index c571d5c..f2da53b 100644 --- a/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift +++ b/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift @@ -89,10 +89,10 @@ extension View { @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.") + @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( of value: Value, debounceTime: Duration, @@ -183,10 +183,10 @@ private struct DebouncedChange2ParamViewModifier: ViewModifier where Valu @State private var debouncedTask: Task? func body(content: Content) -> some View { - content.onChange(of: trigger, initial: initial) { a, b in + content.onChange(of: trigger, initial: initial) { lhs, rhs in debouncedTask?.cancel() debouncedTask = Task.delayed(duration: debounceDuration) { - action(a, b) + action(lhs, rhs) } } }