Home > Software design >  Swift Combine Compare String
Swift Combine Compare String

Time:07-30

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)
    }
}
  • Related