NSPredicate with Swift.

With NSPredicate in Objective-C it was easy to filter the NSARRAY and get the desired results ex: In the Array for Food items, if we want to filter results which has food item's having 'ice' in the foodName.

And with Swift it's even getting simpler , with the use of filter method.

With recent version of Swift, the use of NSPredicate is discouraged. Currently with Swift 4.0 the following code fails, but it could work in earlier versions of swift.


Let me explain it to you with simple NSPredicate used along with Swift.

  let foodArray = [Food(foodId: "1", foodName: "Pizza", spiceLevel: 5),
                         Food(foodId: "2", foodName: "Pasta", spiceLevel: 4),
                         Food(foodId: "3", foodName: "IceCream", spiceLevel: 0),
                         Food(foodId: "4", foodName: "Yogurt IceCream", spiceLevel: 0)]
        
     
        
        let foodItemsHavingIce = (foodArray as NSArray).filtered(using: NSPredicate(format: "foodName contains[cd] %@","ice"))

        print("Food Items with Ice = \(foodItemsHavingIce)")

The above code will work completely fine in Swift Version 3.0 or version below 4.X.


Food Items with Ice = [<Beta.Food: 0x6000000ae2e0>, <Beta.Food: 0x6000000ae340>]


But if you happen to run the same code with Version 4.0. It will throw the following error. To Change the swift Version being used, go to Build Settings ->Swift Language Version , change it to Swift 4.0




 *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Beta.Food 0x6000000a8a00> valueForUndefinedKey:]: this class is not key value coding-compliant for the key foodName.'

Why would the same code be a problem in Swift 4.0?
It is well explained in Limiting @objc inherence .


The following are the issues with using existing NSPredicate in Swift
Objective-C selector collisions
- Swift compiler has to create a "thunk" method that Maps from Objective-C to Swift calling convention and it has to be stored with Objective C meta Data. which in turn increases the size of binary.

If you still wish to proceed  with the NSPredicate the Food class needs to be declared with @objc  , as well as the property you want to use for NSPredicate.

The following Code would be answer , if we still want to work with boilerplate code.
One more point to note about it is, if another developer is working with you and he tries to use NSPredicate over lets say foodId, if @objc is not written with property it will Crash with same exception. So, every property that you would like to have in NSPredicate needs to have @objc.

import UIKit

@objc
class Food: NSObject {

    var foodId:String!
    @objc
    var foodName:String!
    
    var spiceLevel:Int!
    
    init(foodId:String,foodName:String, spiceLevel:Int) {
        self.foodId = foodId
        self.foodName = foodName
        self.spiceLevel = spiceLevel
    }
}





Now Lets move to the correct implementation of using Predicate.

Here we don't need @objc to class nor to the property requires @objc.  Also, if you check there is no conversion of Array to NSArray . Its now pretty straight forward.


Find the foodItem which has "ice" keyword in it. 

        let foodItemsHavingIce = foodArray.filter { (fooditem) -> Bool in
            
            return fooditem.foodName.range(of: "ice", options: .caseInsensitive, range: fooditem.foodName.startIndex..<fooditem.foodName.endIndex, locale: nil)?.isEmpty == false
        }
        print("Food Items with Ice = \(foodItemsHavingIce)")

Few Take away points here to understand the closure function.
-> , is Used to seperate parameters from return type
in ,  seperates return type from body
----------------------------------------------------------------------------


Find the foodItem which spice level = 0
        
        let foodItemsWithSpiceLevel0 = foodArray.filter { (foodItem) -> Bool in
            foodItem.spiceLevel == 0
        }
        
        print("Food Items with SpiceLevel 0 \(foodItemsWithSpiceLevel0)")

Here I have omitted , return keyword.
----------------------------------------------------------------------------

This is more shortened syntax of the above example.
        
        let foodItemsWithSpiceLevel0_2 = foodArray.filter {$0.spiceLevel==0 }

        print("Food Items with SpiceLevel 0  \(foodItemsWithSpiceLevel0_2)")

Here I have omitted both , return keyword and parameters types,  $0 means first parameter of the method
----------------------------------------------------------------------------

Comments

Popular posts from this blog

hitTest on iOS to identify the view being hit

CMTimeMakeWithSeconds explained

Custom Editing Control for UITableViewCell , like iPhone Mail Appliation