Optimizing your search will require that you don't repeat costly operations on every item if they can be performed once beforehand.
It would also help to avoid the overhead of special character processing if the search text doesn't justify it (e.g if it has no spaces or special characters itself).
Finally, procedural code (for loops) are optimized more efficiently by the compiler than their "functional" equivalent (.filter / .map) so the traversal of songs should be performed in a loop instead of a .filter()
I would suggest extending the MPMediaQuery class to allow it to provide a filtered list of songs in the most optimal way possible. This has some reuse value and will keep your main logic more readable and maintainable.
On the other hand, if the MPMediaQuery goes back to storage every time, it will be more efficient to create a simple function that takes the array of songs as a parameter.
For example:
extension MPMediaQuery
{
static func songsMatching(_ searchText:String) -> [MPMediaItem]
{
// Common function to turn special characters into spaces
// will have a certain amount of overhead so we will try
// to avoid using it if possible...
func normalizedText(_ text:String) -> String
{
let chars = text.unicodeScalars.enumerated()
.map ({
CharacterSet.punctuationCharacters.contains($1)
|| CharacterSet.whitespaces.contains($1)
? " " : Character($1)
})
return String(chars)
}
// Search string should only be prepared once
// This means that the whole filtering operation needs to be
// included in the optimization.
let searchString = searchText.lowercased()
let cleanedString = normalizedText(searchString)
// based on search string content, we can forgo complex comparisons
// We only need to manage special characters if the search text
// contains any.
// If the search text doesn't contain spaces or special characters
// then removing them from the titles is unnecessary
let simpleFilter = !cleanedString.contains(" ")
// The Swift optimizer is much more efficient compiling procedural
// code (As opposed to the functional style .filter{} and such)
// This can give a 20x speed boost in some cases.
var result:[MPMediaItem] = []
for song in songs().items ?? []
{
guard let title = song.title?.lowercased()
else { continue }
if simpleFilter
{
if title.contains(searchString)
{ result.append(song) }
}
else
{
if normalizedText(title).contains(searchString)
{ result.append(song) }
}
}
return result
}
}