Skip to content

Instantly share code, notes, and snippets.

@JetForMe
Last active March 14, 2024 22:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JetForMe/a49c663a1394637755fae4599390b964 to your computer and use it in GitHub Desktop.
Save JetForMe/a49c663a1394637755fae4599390b964 to your computer and use it in GitHub Desktop.
import AsyncAlgorithms
import SwiftUI
/**
This example shows one possible way to debounce changes from a user typing into a field.
*/
struct
DebouncedTextView : View
{
var
body: some View
{
TextField("Username", text: self.$username)
.onChange(of: self.username)
{ _, inNew in
self.usernameContinuation?.yield(inNew)
}
.task
{
self.usernameChanges = AsyncStream { self.usernameContinuation = $0 }
for await username in self.usernameChanges!.debounce(for: .seconds(0.25))
{
print("username: \(username)")
}
}
}
@State private var username : String = ""
@State private var usernameChanges : AsyncStream<String>?
@State private var usernameContinuation : AsyncStream<String>.Continuation?
}
/**
This version uses `AsyncStream.makeStream` to halve the number of properties required.
*/
struct
DebouncedTextView2 : View
{
var
body: some View
{
TextField("Username", text: self.$username)
.onChange(of: self.username)
{ _, inNew in
self.usernameChanges.1.yield(inNew)
}
.task
{
for await username in self.usernameChanges.0.debounce(for: .seconds(0.25))
{
print("username: \(username)")
}
}
}
@State private var username = ""
@State private var usernameChanges = AsyncStream.makeStream(of: String.self)
}
/**
Like ``DebouncedTextView2``, but uses the view modifier below.
Note that this version loses the `async` context in the handler, and the reason I
needed all of this was to throttle network requests.
*/
struct
DebouncedTextView3 : View
{
var
body: some View
{
TextField("Username", text: self.$username)
.onChange(of: self.username, debounceFor: .seconds(0.3))
{ inOld, inNew in
print("username: \(inOld), \(inNew)")
}
}
@State private var username = ""
}
struct
DebounceChangeOf<V> : ViewModifier
where V : Equatable, V : Sendable
{
let value : V
let initial : Bool
let debounceFor : Duration
let action : (_ oldValue: V, _ newValue: V) -> Void
func
body(content: Content)
-> some View
{
return content
.onChange(of: value, initial: initial)
{ inOld, inNew in
self.valueChanges.continuation.yield((inOld, inNew))
}
.task
{
for await value in self.valueChanges.stream.debounce(for: .seconds(0.25))
{
self.action(value.0, value.1)
}
}
}
typealias ElementType = (V, V)
@State private var valueChanges = AsyncStream.makeStream(of: ElementType.self)
}
extension
View
{
public
func
onChange<V>(of value: V, initial: Bool = false, debounceFor: Duration, _ action: @escaping (_ oldValue: V, _ newValue: V) -> Void)
-> some View where V : Equatable, V : Sendable
{
return self.modifier(DebounceChangeOf(value: value, initial: initial, debounceFor: debounceFor, action: action))
}
public
func
onChange<V>(of value: V, initial: Bool = false, debounceFor: Duration, _ action: @escaping (_ oldValue: V, _ newValue: V) async -> Void)
-> some View where V : Equatable, V : Sendable
{
return self.modifier(DebounceChangeOf(value: value, initial: initial, debounceFor: debounceFor, action: action))
}
}
@JetForMe
Copy link
Author

It should be possible to make a view modifier that looks like .onChange(of:debounceFor:).

@JetForMe
Copy link
Author

I took a stab at wrapping this all up in a view modifier.

@mattmassicotte
Copy link

I was playing around with this, because I think it is a cool idea. What do you think?

struct OnAsyncChange<V>: ViewModifier where V : Equatable, V: Sendable {
    typealias Element = (oldValue: V, newValue: V)
    typealias Action = (AsyncStream<Element>) async -> Void

    @State private var streamPair = AsyncStream<Element>.makeStream()
    private let action: Action
    private let value: V

    init(of value: V, initial: Bool, action: @escaping Action) {
        self.action = action
        self.value = value
    }

    func body(content: Content) -> some View {
        content
            .onChange(of: value, initial: true) { oldValue, newValue in
                streamPair.continuation.yield((oldValue, newValue))
            }
            .task {
                await action(streamPair.stream)
            }
    }
}

@JetForMe
Copy link
Author

I didn't see your comment until now, so my reply is kinda moot after the long thread on Mastodon, but yeah, I think it's a good idea. Allows the client to arbitrarily process the stream of changes. Assuming none of the issues raised on Mastodon are deal breakers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment