I have a function using Combine that provides a list of results based on user entry.
For example: If the user types rain it will display a list of results with the word rain in it.
The data is being stored in a struct and I need to first match on one element and if there are no matches, try matching on another element.
For example:
struct dataSet: Codable, Hashable {
let nameShort: String
let nameLong: String
}
When the user enters a value in the form field, I want it to first look through nameShort, and then if there are no results, look through nameLong.
The second part of the equation is that I need it to match using the entire string, but with separate words.
For example: If the user enters brown, it should look through the nameShort for brown and then the nameLong for brown. However, if there are tons of entries matching brown and the user then types brown chair, I need it to return results that match both of those values.
Likewise, if the user types brow chai, it should still return brown chair as the initial characters match a word in the struct, even if nameLong is Brown - Side Chair.
Here's an example of my current function:
func editingChangedName(_ value: String) {
$myName
.debounce(for: 0.3, scheduler: RunLoop.main)
.receive(on: DispatchQueue.global()) // Perform filter on background
.map { [weak self] filterString in
guard filterString.count >= 3, let self = self else { return [] }
return self.nameArray.filter {
$0.nameShort
.lowercased()
.contains(
filterString.lowercased()
) ||
$0.nameLong
.lowercased()
.contains(
filterString.lowercased()
)
}
}
.receive(on: RunLoop.main) // Switch back to main thread
.assign(to: &$allNamesArray)
} // End func
This runs onChange of the form field so it's constantly updating the results.
I've tried things like:
let searchString = filterString.lowercased().components(separatedBy: " ")
below the guard statements, and then removed $0.nameShort and $0.nameLong from the return, replacing it with:
searchString.contains(where: $0.nameLong.contains)
but then all the results get screwy.
If I remove $0.nameShort and only use $0.nameLong, and change .contains to .hasPrefix it will only read from left to right and match exactly those characters that exist. So, if I was to type chair I would get 0 results, whereas if I typed brown I would get all the results that start with brown.
I feel like I'm close but can't figure out how to do this properly.
CodePudding user response:
Inside of the map closure try this:
let filterComponents = filterString
.lowercased()
.components(separatedBy: " ")
return self.nameArray.filter { name in
return filterComponents.allSatisfy { component in
return name.nameShort.lowercased().contains(component)
|| name.nameLong.lowercased().contains(component)
}
}
Use String.components(separatedBy:)
to extract each word. Then call allSatisfy
to check if each word is contained in nameShort
or nameLong
.
Note, if nameShort is "brown" and nameLong is "chair" and you use "brown chair" as a search string the data set will match. If you want all the components to match in nameShort or in nameLong you have to do something like this:
let filterComponents = filterString
.lowercased()
.components(separatedBy: " ")
return self.nameArray.filter { name in
return filterComponents.allSatisfy { component in
return name.nameShort.lowercased().contains(component)
} || filterComponents.allSatisfy { component in
return name.nameLong.lowercased().contains(component)
}
}