Update native iOS search filter to use AND/OR (#1298)

This commit is contained in:
Leendert de Borst
2025-10-27 12:56:00 +01:00
committed by Leendert de Borst
parent 9da88cc7e7
commit 4ba2c8e6ab
2 changed files with 58 additions and 48 deletions

View File

@@ -227,38 +227,44 @@ public class PasskeyProviderViewModel: ObservableObject {
}
func filterCredentials() {
if searchText.isEmpty {
let lowercasedSearch = searchText.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
if lowercasedSearch.isEmpty {
filteredCredentials = credentials
} else {
let lowercasedSearch = searchText.lowercased()
filteredCredentials = credentials.filter { credential in
// Filter by service name
if let serviceName = credential.service.name?.lowercased(),
serviceName.contains(lowercasedSearch) {
return true
return
}
// Split search term into words for AND search
let searchWords = lowercasedSearch
.components(separatedBy: .whitespacesAndNewlines)
.filter { !$0.isEmpty }
if searchWords.isEmpty {
filteredCredentials = credentials
return
}
// Filter credentials where ALL search words match (each in at least one field)
filteredCredentials = credentials.filter { credential in
// Prepare searchable fields including passkey rpIds
var searchableFields = [
credential.service.name?.lowercased() ?? "",
credential.service.url?.lowercased() ?? "",
credential.username?.lowercased() ?? "",
credential.alias?.email?.lowercased() ?? "",
credential.notes?.lowercased() ?? ""
]
// Add passkey rpIds to searchable fields
if let passkeys = credential.passkeys {
searchableFields.append(contentsOf: passkeys.map { $0.rpId.lowercased() })
}
// All search words must be found (each in at least one field)
return searchWords.allSatisfy { word in
searchableFields.contains { field in
field.contains(word)
}
// Filter by service URL
if let serviceUrl = credential.service.url?.lowercased(),
serviceUrl.contains(lowercasedSearch) {
return true
}
// Filter by username
if let username = credential.username?.lowercased(),
username.contains(lowercasedSearch) {
return true
}
// Filter by email
if let email = credential.alias?.email?.lowercased(),
email.contains(lowercasedSearch) {
return true
}
// Filter by passkey rpId
if let passkeys = credential.passkeys {
return passkeys.contains { passkey in
passkey.rpId.lowercased().contains(lowercasedSearch)
}
}
return false
}
}
}

View File

@@ -231,30 +231,34 @@ public class CredentialFilter {
return Array(matches)
} else {
// Non-URL fallback: Extract words from search text for better matching
let searchWords = extractWords(from: searchText)
// Non-URL fallback: Multi-word AND search for better matching
let lowercasedSearch = searchText.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
// Split search term into words for AND search
let searchWords = lowercasedSearch
.components(separatedBy: .whitespacesAndNewlines)
.filter { !$0.isEmpty }
if searchWords.isEmpty {
// If no meaningful words after extraction, fall back to simple contains
let lowercasedSearch = searchText.lowercased()
return credentials.filter { credential in
(credential.service.name?.lowercased().contains(lowercasedSearch) ?? false) ||
(credential.username?.lowercased().contains(lowercasedSearch) ?? false) ||
(credential.notes?.lowercased().contains(lowercasedSearch) ?? false)
}
return credentials
}
// Match using extracted words
// Filter credentials where ALL search words match (each in at least one field)
return credentials.filter { credential in
let serviceNameWords = credential.service.name.map { extractWords(from: $0) } ?? []
let usernameWords = credential.username.map { extractWords(from: $0) } ?? []
let notesWords = credential.notes.map { extractWords(from: $0) } ?? []
// Prepare searchable fields
let searchableFields = [
credential.service.name?.lowercased() ?? "",
credential.username?.lowercased() ?? "",
credential.alias?.email?.lowercased() ?? "",
credential.service.url?.lowercased() ?? "",
credential.notes?.lowercased() ?? ""
]
// Check if any search word matches any credential word exactly
return searchWords.contains { searchWord in
serviceNameWords.contains(searchWord) ||
usernameWords.contains(searchWord) ||
notesWords.contains(searchWord)
// All search words must be found (each in at least one field)
return searchWords.allSatisfy { word in
searchableFields.contains { field in
field.contains(word)
}
}
}
}