swiftgenericsasync-awaitcombinedebouncing

Pass generic type T through withCheckedContinuation


I have a search for multiple types, so the general search method has a generic type T, and multiple simple methods call it specifying the type as needed. I'd like to delay search method's start (debounce in Combine) and I'd like to hide this code as deep as I can.

I'm generally using async/await API in my project, only using Combine if I have to. I didn't find a built-in solution for this using async/await so I'm gonna use Combine's debounce. (Please let me know if there is a cleaner solution without Combine)

So I have to go from async/await to Combine and back. This requires to use withCheckedContinuation which loses the T and causes an error:

// simple generic specification using Event
func searchEvents(query: String) async -> [Event] {
    await search(query: query)
}

// simple generic specification using Artist
func searchArtists(query: String) async -> [Artist] {
    await search(query: query)
}

// debouncing layer between specifications and generic search
private func search<T>(query: String) async -> [T] where T: Codable, T: Identifiable  {
    await withCheckedContinuation { continuation in
        Debouncer().debounce(query) { query in
            Task {
                // Generic parameter 'T' could not be inferred
                let a = await self.performSearch(query: query) // <- error here
                continuation.resume(returning: a)
            }
        }
    }
}

// generic search function, details don't matter
private func performSearch<T>(query: String) async -> [T] where T: Codable, T: Identifiable {
    ...
}

// class performing simple debouncing
class Debouncer {

    static var shared = Debouncer()

    private var debounceSubscription: AnyCancellable?

    func debounce(_ string: String, block: (String) -> ()) {
        debounceSubscription?.cancel()
        debounceSubscription = Just(string)
            .debounce(for: .milliseconds(200), scheduler: DispatchQueue.global(qos: .background))
            .sink { string in
                block(string)
            }
    }
}

Is there an elegant way to make it auto-understand the type without passing it as a parameter (type: T.Type, query: String)?

EDIT: Yeah, this thing doesn't work anyway. In the end I had to use Combine on a TextField itself like this:

    .onChange(query) { query in
        queryPublisher.send(query)
    }
    .onReceive(queryPublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { query in
        Task {
            options = await fetchOptions(query)
        }
    }

I wanted to avoid this because there are a lot of text fields using search in the app, but creating a base class for them is easier than hiding debounce deeper I guess. Thanks everyone for the wonderful suggestions, and for the fix with generics which I now know how to use!


Solution

  • Tell the compiler that performSearch will return an array of T

    let a: [T] = await performSearch(query: query)
    

    Now T in performSearch will be the same as T in search which the compiler can conclude what type it is.