I need to filter my model data with a search bar. I added the .searchable() property and when the search text changes I filter my objects with fuzzy matching. This takes too much time and the app lags when writing into the search box. So I want to do the searching asynchronously so that the app doesn't freeze.
I tried to do it with the onChange(of:) property and then I create a Task that runs the async function because the onChange() property doesn't allow async functions by themselves. But the app still lags.
Here is a code example of how I tried doing it:
import SwiftUI
import Fuse
struct SearchView: View {
#EnvironmentObject var modelData: ModelData
#State var searchText = ""
#State var searchResults: [Item] = []
#State var searchTask: Task<(), Never>? = nil
let fuseSearch = Fuse()
var body: some View {
// Show search results
}
.searchable(text: $searchText)
.onChange(of: searchText) { newQuery in
// Cancel if still searching
searchTask?.cancel()
searchTask = Task {
searchResults = await fuzzyMatch(items: modelData.items, searchText: newQuery)
}
}
func fuzzyMatch(items: [Item], searchText: String) async -> [Item] {
filteredItems = items.filter {
(fuseSearch.search(searchText, in: $0.name)?.score ?? 1) < 0.25
}
return filteredItems
}
}
I would really appreciate some help.
I think the main problem is debouncing as lorem ipsum mentioned before.
I just tested my code and you need to call your filter method where i printed.
In this way you will not filter for every editing textfield. You will filter after some millisecond which you may change.
You can find more detail in this link
SwiftUI Combine Debounce TextField
struct Example: View {
#State var searchText = ""
let searchTextPublisher = PassthroughSubject<String, Never>()
var body: some View {
NavigationView {
Text("Test")
}
.searchable(text: $searchText)
.onChange(of: searchText) { searchText in
searchTextPublisher.send(searchText)
}
.onReceive(
searchTextPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
) { debouncedSearchText in
print("call your filter method")
}
}
}
If you want to introduce debouncing, you can just add a Task.sleep:
.onChange(of: searchText) { newQuery in
// Cancel if still searching
searchTask?.cancel()
searchTask = Task {
try await Task.sleep(for .seconds(0.5))
searchResults = await fuzzyMatch(items: modelData.items, searchText: newQuery)
}
}
If you do that, you will have to change searchTask to be Task<(), Error>?.
However, debouncing might not be the whole issue. If your fuzzy filtering is too slow, you might need to make it run asynchronously and get it off the main thread:
The fuzzyMatch is marked as async, but is not currently doing anything asynchronous. Having the method signature reflect what is going on may make it easier to reason about one’s code.
To that end, now that we realize that fuzzyMatch runs synchronously, it becomes apparent that if it runs slowly, it will block the current thread. And because Task { ... } runs on the current actor, you will end up blocking the main thread. You should consider using Task.detached to get it on a background thread. But mark searchResults as being on the main actor.
If you cancel the Task, it will not stop fuzzyMatch that is underway. It should check for cancelation.
So, pulling that all together, perhaps:
struct ContentView: View {
#StateObject var modelData: ModelData()
#State var searchText = ""
#MainActor #State var searchResults: [Item] = []
#State var searchTask: Task<[Item], Error>?
let fuseSearch = Fuse()
var body: some View {
NavigationStack {
...
}
.searchable(text: $searchText)
.onChange(of: searchText) { newQuery in
Task {
searchTask?.cancel()
let task = Task.detached {
try await Task.sleep(for: .seconds(0.5)) // debounce; if you don't want debouncing, remove this, but it can eliminate annoying updates of the UI while the user is typing
return try await fuzzyMatch(items: modelData.items, searchText: newQuery)
}
searchTask = task
searchResults = try await task.value
}
}
}
func fuzzyMatch(items: [Item], searchText: String) throws -> [Item] {
try items.filter {
try Task.checkCancellation()
return (fuseSearch.search(searchText, in: $0.name)?.score ?? 1) < 0.25
}
}
}
Related
In a view, I want to wait for a series of async calls to finish loading, then redirect to another screen. Unfortunately, I see the code running in the back (The JSON data gets loaded) but once it completes it does not redirect to the new view.
Here is my view:
struct loadingView: View {
#ObservedObject var dataLoader: DataLoader = DataLoader()
#State var isLoaded: Bool = false
var body: some View {
VStack {
Text("Loading \(isLoaded)")
}
}
.task {
await self.dataloader.loadJSONData(isLoaded: $isLoaded)
MainScreen()
}
}
...and the DataLoader class:
#MainActor DataLoader: NSObject, ObservableObject {
func loadJSONData(isLoaded: Binding<Bool>) {
await doLoadData()
isLoaded.wrappedValue = True
}
func doLoadData() async {
/* do data load */
/* This code works */
}
}
"Redirecting" here doesn't really make sense. Do you really want the user to be able to navigate back to the loading screen? Perhaps you're thinking of this like a web page, but SwiftUI is nothing like that. What you really want to do is display one thing when loading, and a different thing when loaded. That's just if, not "redirection."
Instead, consider the following pattern. Create this kind of LoadingView (extracted from some personal code of mine):
struct LoadingView<Content: View, Model>: View {
enum LoadState {
case loading
case loaded(Model)
case error(Error)
}
#ViewBuilder let content: (Model) -> Content
let loader: () async throws -> Model
#State var loadState = LoadState.loading
var body: some View {
ZStack {
Color.white
switch loadState {
case .loading: Text("Loading")
case .loaded(let model): content(model)
case .error(let error): Text(verbatim: "Error: \(error)")
}
}
.task {
do {
loadState = .loaded(try await loader())
} catch {
loadState = .error(error)
}
}
}
}
It require no redirection. It just displays different things when in different states (obviously the Text view can be replaced by something more interesting).
Then to use this, embed it in another View. In my personal code, that includes a view like this:
struct DailyView: View {
var body: some View {
LoadingView() { model in
LoadedDailyView(model: model)
} loader: {
try await DailyModel()
}
}
}
Then LoadedDailyView is the "real" view. It is handled a fully populated model that is created by DailyModel.init (a throwing, async init).
You could try this approach, using NavigationStack and NavigationPath to Redirecting after task w/ Await completes.
Here is the code I use to test my answer:
struct ContentView: View {
var body: some View {
loadingView()
}
}
#MainActor
class DataLoader: NSObject, ObservableObject {
func loadJSONData() async {
await doLoadData()
// for testing, wait for 1 second
try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)
}
func doLoadData() async {
/* do data load */
/* This code works */
}
}
struct loadingView: View {
#StateObject var dataLoader = DataLoader()
#State private var navPath = NavigationPath()
var body: some View {
NavigationStack(path: $navPath) {
VStack (spacing: 44) {
Text("Loading....")
}
.navigationDestination(for: Bool.self) { _ in
MainScreen()
}
}
.task {
await dataLoader.loadJSONData()
navPath.append(true)
}
}
}
struct MainScreen: View {
var body: some View {
Text("---> MainScreen here <---")
}
}
If you need ios 15 or earlier, then use NavigationView:
struct loadingView: View {
#StateObject var dataLoader = DataLoader()
#State var isLoaded: Bool?
var body: some View {
NavigationView {
VStack {
Text(isLoaded == nil ? "Loading..." : "Finished loading")
NavigationLink("", destination: MainScreen(), tag: true, selection: $isLoaded)
}
}.navigationViewStyle(.stack)
.task {
await dataLoader.loadJSONData()
isLoaded = true
}
}
}
If your loadingView has the only purpose of showing the "loading" message, then
display the MainScreen after the data is loaded, you could use the following approach using a simple swicth:
struct loadingView: View {
#StateObject var dataLoader = DataLoader()
#State private var isLoaded = false
var body: some View {
VStack {
if isLoaded {
MainScreen()
} else {
ProgressView("Loading")
}
}
.task {
await dataLoader.loadJSONData()
isLoaded = true
}
}
}
Use #StateObject instead of #ObservedObject. Use #Published instead of trying to pass a binding to the object (that is a mistake because a binding is just a pair of get and set closures that will expire if LoadingView is re-init), use Group with an if to conditionally show a View e.g.
struct LoadingView: View {
#StateObject var dataLoader: DataLoader = DataLoader()
var body: some View {
Group {
if dataLoader.isLoaded {
LoadedView(data: dataLoader.data)
} else {
Text("Loading...")
}
}
.task {
await dataloader.loadJSONData()
}
}
The DataLoader should not be #MainActor because you want it to run on a background thread. Use #MainActor instead on a sub-task once the async work has finished e.g.
class DataLoader: ObservableObject {
#Published var isLoaded = false
#Published var data: [Data] = []
func loadJSONData async {
let d = await doLoadData()
Task { #MainActor in
isLoaded = true
data = d
}
}
func doLoadData() async {
/* do data load */
/* This code works */
}
}
This pattern is shown in Apple's tutorial here, PandaCollectionFetcher.swift copied below:
import SwiftUI
class PandaCollectionFetcher: ObservableObject {
#Published var imageData = PandaCollection(sample: [Panda.defaultPanda])
#Published var currentPanda = Panda.defaultPanda
let urlString = "http://playgrounds-cdn.apple.com/assets/pandaData.json"
enum FetchError: Error {
case badRequest
case badJSON
}
func fetchData() async
throws {
guard let url = URL(string: urlString) else { return }
let (data, response) = try await URLSession.shared.data(for: URLRequest(url: url))
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badRequest }
Task { #MainActor in
imageData = try JSONDecoder().decode(PandaCollection.self, from: data)
}
}
}
I'm having trouble making async functions run in background threads (to prevent blocking the main thread).
Below is a method that takes about 5 seconds to run.
From what I've learned, it seemed like making the function async and marking it with await on function call would be enough. But it doesn't work as intended and still freezes up the UI.
EDIT
Since it's stated that Swift 5.5 concurrency can replace DispatchQueue, I am trying to find a way to do this with only Async/Await.
EDIT_2
I did try removing the #MainActor wrapper, but it still seem to run on the main thread.
NumberManager.swift
#MainActor class NumberManager: ObservableObject {
#Published var numbers: [Double]?
func generateNumbers() async {
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
self.numbers = numbers
// takes about 5 seconds to run...
} }
ContentView
struct ContentView: View {
#StateObject private var numberManager = NumberManager()
var body: some View{
TabView{
VStack{
DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
.onAppear() {
Task {
// Runs in the main thread, freezing up the UI until it completes.
await numberManager.generateNumbers()
}
}
}
.tabItem {
Label("One", systemImage: "list.dash")
}
Text("Hello")
.tabItem {
Label("Two", systemImage: "square.and.pencil")
}
}
}
var isNumbersValid: Bool{
numberManager.numbers != nil && numberManager.numbers?.count != 0
} }
What I've tried...
I've tried a few things, but the only way that made it run in the background was changing the function as below. But I know that using Task.detached should be avoided unless it's absolutely necessary, and I didn't think this is the correct use-case.
func generateNumbers() async {
Task.detached {
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
await MainActor.run { [numbers] in
self.numbers = numbers
}
}
Writing async on a function doesn’t make it leave the thread. You need a continuation and you need to actually leave the thread somehow.
Some ways you can leave the thread using DispatchQueue.global(qos: .background).async { or use Task.detached.
But the most important part is returning to the main thread or even more specific to the Actor's thread.
DispatchQueue.main.async is the "old" way of returning to the main thread it shouldn't be used with async await. Apple as provided CheckedContinuation and UncheckedContinuation for this purpose.
Meet async/await can elaborate some more.
import SwiftUI
struct ConcurrentSampleView: View {
//Solution
#StateObject var vm: AsyncNumberManager = .init()
//Just to create a project that can show both scenarios.
//#StateObject var vm: NumberManager = .init()
#State var isLoading: Bool = false
var body: some View {
HStack{
//Just to visualize the thread being released
//If you use NumberManager the ProgressView won't appear
//If you use AsyncNumberManager the ProgressView WILL appear
if isLoading{
ProgressView()
}
Text(vm.numbers == nil ? "nil" : "\(vm.numbers?.count.description ?? "")")
}
//.task is better for iOS 15+
.onAppear() {
Task{
isLoading = true
await vm.generateNumbers()
isLoading = false
}
}
}
}
struct ConcurrentSampleView_Previews: PreviewProvider {
static var previews: some View {
ConcurrentSampleView()
}
}
#MainActor
class AsyncNumberManager: ObservableObject {
#Published var numbers: [Double]?
func generateNumbers() async {
numbers = await concurrentGenerateNumbers()
}
private func concurrentGenerateNumbers() async -> [Double] {
typealias Cont = CheckedContinuation<[Double], Never>
return await withCheckedContinuation { (cont: Cont) in
// This is the asynchronous part, have the operation leave the current actor's thread.
//Change the priority as needed
//https://developer.apple.com/documentation/swift/taskpriority
Task.detached(priority: .utility){
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
//This tells the function to return to the actor's thread
cont.resume(returning: numbers)
}
}
}
//Or something like this it just depends on the true scenario
private func concurrentGenerateNumbers2() async -> [Double] {
// This is the asynchronous part, have the operation leave the actor's thread
//Change the priority as needed
//https://developer.apple.com/documentation/swift/taskpriority
return await Task.detached(priority: .utility){
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
return numbers
}.value
}
}
//Incorrect way of applying async/await. This doesn't actually leave the thread or mark when to return. Left here to highlight both scenarios in a reproducible example.
#MainActor
class NumberManager: ObservableObject {
#Published var numbers: [Double]?
func generateNumbers() async {
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
self.numbers = numbers
}
}
You have answered your own question - You can use structured concurrency to solve your problem.
Your problem occurs because you have use the #MainActor decorator on your class. This means that it executes on the main queue.
You can either remove this decorator or, as you have found, use structured concurrency to explicitly create a detached task and the use a main queue task to provide your result.
Which approach you use depends on what else this class does. If it does a lot of other work that needs to be on the main queue then #MainActor is probably a good approach. If not then remove it.
The #MainActor property wrapper is not (just) what makes your observable object run on the main thread. #StateObject is what's doing it. Which is logical, since changes to the object will update the UI.
Removing the #MainActor wrapper is not the solution, because any changes to #Published properties will have to be done on the main thread (since they update the UI). You also don't want to run Task.detached, at least not if the task is going to change any #Published property, for the same reason.
By marking your generateNumbers function as async, a method from the main thread can call it with await - which allows the task to suspend and not block the main thread. That's what makes it concurrent.
extension NumberManager {
func loadNumbers() {
// This task will run on the main thread
Task {
// `await` tells the Swift executor that this method can run in the background,
// and the main thread can continue doing other things while it waits for its result
self.numbers = await self.generateNumbers()
}
}
func generateNumbers() async -> [Double] {
return (1...10_000_000).map { _ in Double.random(in: -10...10)
}
}
struct ContentView: View {
#StateObject private var numberManager = NumberManager()
var body: some View {
TabView{
VStack{
DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
}
}
.onAppear { numberManager.loadNumbers() }
}
}
A more complete loadNumbers method could also store a reference to the task allowing you to cancel or restart a running task. However nowadays we have the excellent .task(priority:_:) to do all of that for you. It manages the task's lifecycle automatically, which means less boilerplate code.
struct ContentView: View {
#StateObject private var numberManager = NumberManager()
var body: some View {
TabView{
VStack{
DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
}
}
// generate numbers
.task(.priority: .high) {
numberManager.numbers = await numberManager.generateNumbers
}
}
}
As far as I know this is the most succinct way to do expensive calculations on a background thread in Swift 5.7.
I want to load data from an API, then pass that data to several child views.
Here's a minimal example with one child view (DetailsView). I am getting this error:
Cannot convert value of type 'Binding<Subject>' to expected argument type 'BusinessDetails'
import Foundation
import SwiftUI
import Alamofire
struct BusinessView: View {
var shop: Business
class Observer : ObservableObject{
#Published public var shop = BusinessDetails()
#Published public var loading = false
init(){ shop = await getDetails(id: shop.id) }
func getDetails(id: String) async -> (BusinessDetails) {
let params = [
id: id
]
self.loading = true
self.shop = try await AF.request("https://api.com/details", parameters: params).serializingDecodable(BusinessDetails.self).value
self.loading = false
return self.shop
}
}
#StateObject var observed = Observer()
var body: some View {
if !observed.loading {
TabView {
DetailsView(shop: $observed.shop)
.tabItem {
Label("Details", systemImage: "")
}
}
}
}
}
This has worked before when the Observed object's property wasn't an object itself (like how the loading property doesn't cause an error).
When using async/await you should use the .task modifier and remove the object. The task will be started when the view appears, cancelled when it disappears and restarted when the id changes. This saves you a lot of effort trying to link async task lifecycle to object lifecycle. e.g.
struct BusinessView: View {
let shop: Business
#State var shopDetails = BusinessDetails()
#State var loading = false
var body: some View {
if loading {
Text("Loading")
}
else {
TabView {
DetailsView(shop: shopDetails)
.tabItem {
Label("Details", systemImage: "")
}
}
}
.task(id: shop.id) {
loading = true
shopDetails = await Self.getDetails(id: shop.id) // usually we have a try catch around this so we can show an error message
loading = false
}
}
// you can move this func somewhere else if you like
static func getDetails(id: String) async -> BusinessDetails{
let params = [
id: id
]
let result = try await AF.request("https://api.com/details", parameters: params).serializingDecodable(BusinessDetails.self).value
return result
}
}
}
I want to implement a Text field that displays the current user's existing score in the DB (Firestore). Because of the nature of async in Firebase query, I also need to do some adjustment in my codes. However, it seems that completion() handler does not work well:
// ViewModel.swift
import Foundation
import Firebase
import FirebaseFirestore
class UserViewModel: ObservableObject {
let current_user_id = Auth.auth().currentUser!.uid
private var db = Firestore.firestore()
#Published var xp:Int?
func fetchData(completion: #escaping () -> Void) {
let docRef = db.collection("users").document(current_user_id)
docRef.getDocument { snapshot, error in
print(error ?? "No error.")
self.xp = 0
guard let snapshot = snapshot else {
completion()
return
}
self.xp = (snapshot.data()!["xp"] as! Int)
completion()
}
}
}
// View.swift
import SwiftUI
import CoreData
import Firebase
{
#ObservedObject private var users = UserViewModel()
var body: some View {
VStack {
HStack {
// ...
Text("xp: \(users.xp ?? 0)")
// Text("xp: 1500")
.fontWeight(.bold)
.padding(.horizontal)
.foregroundColor(Color.white)
.background(Color("Black"))
.clipShape(CustomCorner(corners: [.bottomLeft, .bottomRight, .topRight, .topLeft], size: 3))
.padding(.trailing)
}
.padding(.top)
.onAppear() {
self.users.fetchData()
}
// ...
}
}
My result kept showing 0 in Text("xp: \(users.xp ?? 0)"), which represents that the step is yet to be async'ed. So what can I do to resolve it?
I would first check to make sure the data is valid in the Firestore console before debugging further. That said, you can do away with the completion handler if you're using observable objects and you should unwrap the data safely. Errors can always happen over network calls so always safely unwrap anything that comes across them. Also, make use of the idiomatic get() method in the Firestore API, it makes code easier to read.
That also said, the problem is your call to fetch data manually in the horizontal stack's onAppear method. This pattern can produce unsavory results in SwiftUI, so simply remove the call to manually fetch data in the view and perform it automatically in the view model's initializer.
class UserViewModel: ObservableObject {
#Published var xp: Int?
init() {
guard let uid = Auth.auth().currentUser?.uid else {
return
}
let docRef = Firestore.firestore().collection("users").document(uid)
docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let xp = doc.get("xp") as? Int {
self.xp = xp
} else if let error = error {
print(error)
}
}
}
}
struct ContentView: View {
#ObservedObject var users = UserViewModel()
var body: some View {
VStack {
HStack {
Text("xp: \(users.xp ?? 0)")
}
}
}
}
SwiftUI View - viewDidLoad()? is the problem you ultimately want to solve.
I am trying to implement a search bar in my app, as now I want to use the keyword typed in the search bar to make an API call to fetch backend data, here is my code:
struct SearchView: View {
#State private var searchText : String=""
#ObservedObject var results:getSearchList
init(){
results = SearchList(idStr: self.searchText)
}
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText)
}.navigationBarTitle(Text("Search"))
}
}
}
I implement SearchBar view followed the this tutorial https://www.appcoda.com/swiftui-search-bar/ exactly,
and getSearchList is a class which has an var called idStr,
struct searchResEntry: Codable, Identifiable{
var id:Int
var comment:String
}
class SearchList: ObservableObject {
// 1.
#Published var todos = [searchResEntry]()
var idStr: String
init(idStr: String) {
self.idStr = idStr
let url = URL(string: "https://..." + idStr)!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let todoData = data {
// 3.
let decodedData = try JSONDecoder().decode([searchResEntry].self, from: todoData)
DispatchQueue.main.async {
self.todos = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
the problem I am struggling now is that I want to use the variable searchText to initialize the getSearchList , getSearchList has an var called idStr, this idStr is to used to store the typed keyword, my code always get an error: 'self' used before all stored properties are initialized , I have no idea how to deal with this.
Here is your code, edited by me:
struct SearchView: View {
#StateObject var results = SearchList()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $results.searchText)
}.navigationBarTitle(Text("Search"))
}
}
}
struct SearchResEntry: Codable, Identifiable {
var id:Int
var backdrop_path:String
}
class SearchList: ObservableObject {
#Published var todos = [SearchResEntry]()
#Published var searchText: String = ""
var cancellable: AnyCancellable?
init() {
cancellable = $searchText.debounce(
for: .seconds(0.2),
scheduler: RunLoop.main
).sink { _ in
self.performSearch()
}
}
func performSearch() {
if let pathParam = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
let url = URL(string: "https://hw9node-310902.uc.r.appspot.com/mutisearch/\(pathParam)") {
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let todoData = data {
let decodedData = try JSONDecoder().decode([SearchResEntry].self, from: todoData)
DispatchQueue.main.async {
self.todos = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
} else {
print("Invalid URL")
}
}
}
Explanation
You are free to reverse the optional changes i made, but here are my explanations:
Use capital letter at the beginning of a Type's name. e.g write struct SearchResEntry, don't write struct searchResEntry. This is convention. Nothing big will happen if you don't follow conventions, but if anyone other than you (or maybe even you in 6 months) look at that code, chances are they go dizzy.
Dont start a Type's name with verbs like get! Again, this is just a convention. If anyone sees a getSomething() or even GetSomething() they'll think thats a function, not a Type.
Let the searchText be a published property in your model that performs the search. Don't perform search on init, instead use a function so you can initilize once and perform search any time you want (do results.performSearch() in your View). Also you can still turn your searchText into a binding to pass to your search bar (look at how i did it).
EDIT answer to your comment
I could right-away think of 3 different answers to your comment. This is the best of them, but also the most complicated one. Hopefully i chose the right option:
As you can see in the class SearchList i've added 2 things. First one is a cancellable to store an AnyCancellable, and second is the thing in init() { ... }. In init, we are doing something which results in an AnyCancellable and then we are storing that in the variable that i added.
What am i doing In init?
first $searchText gives us a Publisher. Basically, the publisher is called whenever the searchText value changes. Then you see .debounce(for: .seconds(0.2), on: RunLoop.main) which means only let the latest input go through and reach the next thing (the next thing is .sink { } as you can see), only if the user has stopped writing for 0.2 seconds. This is very helpful to avoid a load of requests to the server which can eventually make servers give you a 429 Too Many Requests error if many people are using your app (You can remove the whole .debounce thing if you don't like it). And the last thing is .sink { } which when any value reaches that point, it'll call the performSearch func for you and new results will be acquired from the server.
Alternative way
(again talking about your comment)
This is the simpler way. Do as follows:
remove init() { ... } completely if you've added it
remove var cancellable completely if you've added it
in your SearchView, do:
.onChange(of: results.searchText) { _ in
results.performSearch()
}
pretty self-explanatory; it'll perform the search anytime the searchText value is changed.