3

I have array of structs and I don't know really how to use search by one of the struct's parameter.

My struct looks like:

struct Actor {
   var name: String!
   var posterURL: String!

    init(_ dictionary: [String: Any]) {
       name = dictionary["name"] as! String
       posterURL = dictionary["image"] as! String
    }
}

So, I've tried to use predicate

let actorSearchPredicate = NSPredicate(format: "name contains[c] %@", text)
filterredActors = (actors as NSArray).filtered(using: actorSearchPredicate)

But, when I'm trying to type anything, it crashes with error

[<_SwiftValue 0x10657ffb0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.'

What is the right way of using predicate search with structs? Thank you

djromero
  • 19,551
  • 4
  • 71
  • 68
Vitalyz123
  • 189
  • 2
  • 10
  • CHeck https://stackoverflow.com/questions/44762460/swift-4-this-class-is-not-key-value-coding-compliant/44790231 – shallowThought Jan 12 '18 at 20:08
  • 6
    Why bother with `NSPredicate`? `actors.filtered{ $0.name.contains(text) }` – Alexander Jan 12 '18 at 20:09
  • Only thing missing with `$0.name.contains(text)` is making it case insensitive. – creeperspeak Jan 12 '18 at 20:19
  • 1
    One really easy way of making it case insensitive is `$0.name.lowercased().contains(text.lowercased())`. – creeperspeak Jan 12 '18 at 20:23
  • @creeperspeak filterredActors = actors.filter{($0 as AnyObject).name.lowercased().contains(text.lowercased())} I did like this, but it continues crushing – Vitalyz123 Jan 12 '18 at 20:38
  • 1
    Why are you casting as `AnyObject`? You shouldn't be using that at all in Swift 4. Try `actors.filtered{( $0.name.lowercased().contains(text.lowercased()) )}`. Also, there is a difference between `filter` and `filtered`. Make sure you're using `filtered`. – creeperspeak Jan 12 '18 at 20:40
  • @creeperspeak better to use caseInsensitiveCompare https://stackoverflow.com/questions/31329568/check-if-a-string-exists-in-an-array-case-insensitively/31330465?s=2|54.9670#31330465 or you can create your own caseInsensitiveContains https://stackoverflow.com/a/41753828/2303865 – Leo Dabus Jan 12 '18 at 21:11
  • @Vitalyz123 You should avoid as much as possible the use of implicitly unwrapped optionals and make your properties constants `let name: String` and naming a string URL is misleading, change its type to a real URL – Leo Dabus Jan 12 '18 at 21:18
  • @creeperspeak thanks, it worked but with this way "filterredActors = actors.filter{( $0.name.lowercased().contains(text.lowercased()) )}." What is the difference between 'filter' and 'filtered' ? Swift only offers 'filter' option – Vitalyz123 Jan 12 '18 at 21:27
  • Sorry - I got it mixed up with the difference between `sort` and `sorted`. – creeperspeak Jan 12 '18 at 21:42

2 Answers2

3

NSPredicate is not needed, Swift got convenience methods to check case insensitive

whether the receiver contains a given string by performing a case insensitive, locale-aware search.

filterredActors = actors.filter { $0.name.localizedCaseInsensitiveContains(text) }

and case and diacritic insensitive

whether the receiver contains a given string by performing a case and diacritic insensitive, locale-aware search.

filterredActors = actors.filter { $0.name.localizedStandardContains(text) }
vadian
  • 274,689
  • 30
  • 353
  • 361
2

If you need to use a predicate to filter structs, you can do it without going into @objc territory or converting your data into NSArray:

func filter(actors: [Actor], named: String) -> [Actor] {
    let predicate = NSPredicate(format: "SELF contains[cd] %@", named)
    return actors.filter {
        predicate.evaluate(with: $0.name)
    }
}

Example:

struct Actor {
    let name: String
    let imdb: String
}

let actors = [
    Actor(name: "John Travolta", imdb: "http://www.imdb.com/name/nm0000237"),
    Actor(name: "Uma Thurman", imdb: "http://www.imdb.com/name/nm0000235"),
    Actor(name: "Samuel L. Jackson", imdb: "http://www.imdb.com/name/nm0000 168"),
    Actor(name: "Penélope Cruz", imdb: "http://www.imdb.com/name/nm0004851"),
    Actor(name: "Penelope Ann Miller", imdb: "http://www.imdb.com/name/nm0000542")
]

print(filter(actors: actors, named: "Uma").map { $0.name })
// ["Uma Thurman"]
print(filter(actors: actors, named: "J").map { $0.name })
// ["John Travolta", "Samuel L. Jackson"]

Filtering using a predicate is useful, for example, to take into account diacritics. Let's search an actor name with an accented character:

print(filter(actors: actors, named: "Penelope").map { $0.name })
// ["Penélope Cruz", "Penelope Ann Miller"]

It finds both "Penelope", with and without accented "e".

Using String.contains won't work in that case:

let text = "Penelope"
let hits = actors.filter { ( $0.name.lowercased().contains(text.lowercased()) )}
print(hits.map { $0.name })
// ["Penelope Ann Miller"]

There are better alternatives (String localized search, regular expressions, String.range(of:options:range:locale), ...). Using predicates is not faster (not much slower either). It may be easier to reason about complex queries using predicate syntax if you already know it.

djromero
  • 19,551
  • 4
  • 71
  • 68