This swift package lets you Search and Sort any Sendable array on the background thread based on your provided KeyPath(s) in a Type Safe manner.
You should import the package using Swift Package Manager in your project.
To import the package to your existing project simply open File -> Add Package Dependencies and copy paste URL to SPM.
To import to a swift package add the following url to your package dependencies:
.package(url: "https://github.com/mohammad-nej/SearchAndSort", .upToNextMajor(from: "1.0.1")),
you should also add it as a product to your targets :
.product(name: "SearchAndSort", package: "SearchAndSort"),
This package provides different types for Searching , Sorting based on KeyPaths to Sendable types.
Assuming we have a Student type :
struct Student : Sendable {
let name : String
let age : Int
let grade : Double
let birthDate : Date
}if we want to sort an array of Students based on name :
let students : [Studnet] = []
// we fill the students array
let nameKey = SortableKeyPath(\Student.name)
let sortedArray = nameKey.sort(students, order: .ascending)in order to create a SortableKeyPath your type should be Sendable and the key should conform to Comparable protocol otherwise you will get compile-time error.
if you want to be able to sort based on multiple Keys you can use AnySortableKey type erasure which will erase you Key type:
let nameKey = SortableKeyPath(\Student.name)
let ageKey = SortableKeyPath(\Student.age)
let keys : [AnySortableKey] = [.init(nameKey), .init(ageKey)]Note: this will only erase Keys type, thus you can't store SortableKeyPaths of different models in the same array.
in order to be able to search based on a a KeyPath you need to provide a type conforming to Stringifier protocol. This protocol only has 1 method :
public protocol Stringifier : Sendable {
associatedtype Model
func stringify(_ model : Model) -> [String]
}assuming that we want to implement this protocol for Int type :
struct IntStringifier : Stringifier, Sendable {
func stringify(_ model: Int) -> [String] {
var results : [String] = []
results.append(model.formatted())
results.append(model.description)
return results
}
}this way when BackgroundSearcher is iterating over your models, it will converted every Int to an array of strings and match it with your search query.
for example with this IntStringifier your user will get a match for 2000 either if he enters "2000" or "2,000" as query.
Good news: Any type that confroms to CustomStringConvertibale protocol will automaticly receive a Stringifier which will return its description.
with that being said lets create SearchableKey:
let nameKey = SearchableKeyPath(\Student.name)
let ageKey = SearchableKeyPath(\Student.age)
let dateKey = SearchableKeyPath( \Student.birthDate, stringifier: MyOwnStringifier())we can now search on any array of type Student using these keys:
let searchResult = await nameKey.search(in:studnets , for: "John")
let searchResult = await ageKey.search(in:studnets , for: "John", strategy: .exact)Note: search function will return nil if it's canceled before finishing and it will return an empty array if doesn't find anything.
Notes: you can choose between different matching strategies, the default value is .contains.
just like Sorting you can also use AnySearchableKey to type erase your keys and store them in an array:
let keys : [AnySearchableKey] = [.init(nameKey), .init(ageKey)]this is an actor that actually does the search for you. it will create Task.detached(s) internally depending on the size of your array.
You can pass your models and keys to this type and let it do the search for you.
let searcher = BackgroundSearcher(models: studnets,keys: [.init(nameKey),.init(ageKey)])
await searcher.search("John" ,withKey: [.init(nameKey)] , strategy: .prefix)Notes:
- if you don't provide a value for
withKey:parameter it will iterate over all theKeys that you provided during initialization. - when you are calling
searchdirectly from aSearchableKeyPathan instance ofBackgroundSearcheris created internally. BackgroundSearcherwill create multipleTasks if your array size exceeds1500, you can change this number by settingminimumElementsToMultiThread.
just like BackgroundSearcher, Sorter will let you store all your models and keys in one place:
let nameSort = SortableKeyPath(\Student.name)
let ageSort = SortableKeyPath(\Student.age)
let sorter = Sorter(models: students, keys: [.init(nameSort),.init(ageSort))Note: unlike BackgroundSearcher, Sorter will always create 1 Task regardless of the size of your array.
this type can be used to provide a title for your keys ( e.g. you want to show it in the UI).
let titledNameKey = TitledKey(title: "Name", key: \Student.name)this type can be replaced by SearchableKeyPath and you can also use it in place of a SortableKeyPath if the Key that you are providing conforms to Comparable protocol.
so it can be used as an input to BackgroundSearcher , AnySearchableKey and Sorter , AnySortableKey ( if the key is Comparable). the only downside to use this type is that if you only want to do sort, you always have to provide Stringifier for your Key type.
this type is also a type erasure that can contain SortableKey,SearchableKey,TitledKey , or it can even be created directly from KeyPath.
let sortableKey = SortableKeyPath(\Student.name, order: .ascending)
let birthDayKey = SearchableKeyPath(\Student.birthDate,stringifier: .persian)
let nameKey = TitledKey(title: "Name", key: \Student.name)
let anykey = AnyKey(birthDayKey,sortOrder: .ascending)
let anyKey2 = AnyKey(nameKey,sortOrder:.ascending)
let anyKey3 = AnyKey(sortableKey) This package is completely Type Safe and you can't go wrong without getting compile time error. it also based on Swift 6 so it's completely concurrency safe.
This package is provided to use for free. if you want to know more feel free to contact me on GitHub or using my email : mohammad.nej@gmail.com