3

I am attempting to include a function in a Predicate definition. Is this possible?

Let's say you have a Core Data entity of Places with attributes for latitude and longitude.

I would like to add annotations to a mapview of those Places within a specified distance from the user location. I can, of course, loop through the entire database and calculate the distance between each Place and the user location but I will have about 35000 places and it would seem that it would be more efficient to have a predicate in the fetchedResultsController setup.

I tried the code below but I receive an error message of "Problem with subpredicate BLOCKPREDICATE(0x2808237b0) with userInfo of (null)"

func myDistanceFilter(myLat : CLLocationDegrees, myLong : CLLocationDegrees, cdLat : CLLocationDegrees, cdLong : CLLocationDegrees) -> Bool {

    let myLocation = CLLocation(latitude: myLat, longitude: myLong)
    let cdLocation = CLLocation(latitude: cdLat, longitude: cdLong)

    if myLocation.distance(from: cdLocation) < 5000.0 {
        return true
    } else {
        return false
    }
}//myDistancePredicate

And inside the fetchedResultsController:

let distancePredicate = NSPredicate {_,_ in self.myDistanceFilter(myLat: 37.774929, myLong: -122.419418, cdLat: 38.0, cdLong: -122.0)}

If it is possible to have a block/function inside a predicate how do you get a reference to an attribute for the entity object being evaluated?

Any guidance would be appreciated.

Nikunj Kumbhani
  • 3,758
  • 2
  • 26
  • 51
JohnSF
  • 3,736
  • 3
  • 36
  • 72
  • 1
    Are you want to fetch all match values for this? – Nikunj Kumbhani Apr 02 '19 at 04:46
  • Yes. Fetch the matches, populate a tableview and use the result to add annotations to a mapview. – JohnSF Apr 02 '19 at 18:27
  • Rather than trying to define a radius within which you want your annotations to fall (which, as Jerry points out below, cannot be used in a fetch), set a minimum and maximum latitude and longitude. Your predicate will then be “latitude > minLat AND latitude < maxLat AND longitude > minLong AND longitude < maxLong” which can be used in a fetch request. Some of the items returned might be outside the visible bounds of the mapView, but that is of no consequence. I don’t know CLLocation well enough to advise the correct calculations for minLat, maxLat, minLong and maxLong. – pbasdf Apr 02 '19 at 21:07

2 Answers2

3

An additional observation for anyone else struggling with a similar issue.

Considering the suggestions of pbasdf and Jerry above, at least in my case, there is no reason why a region has to be round. I'll craft a name indicating a nearly rectangular area. I ran some tests with latitude and longitude values. These latitude and longitude values can be scaled to a rectangle enclosing a circular radius specified by the user. One degree of latitude is about 69 miles and one degree of longitude at the latitude of Chicago is about 51 miles. I used the following:

var myUserLatitude : CLLocationDegrees!
var myUserLongitude : CLLocationDegrees!

In the init file for the view:

guard let userLatitude = locationManager.location?.coordinate.latitude else {return}
guard let userLongitude = locationManager.location?.coordinate.longitude else {return}
myUserLatitude = userLatitude
myUserLongitude = userLongitude

Inside the fetchedResultsController variable creation:

let latitudeMinPredicate = NSPredicate(format: "latitude >= %lf", myUserLatitude - 1.0)
let latitudeMaxPredicate = NSPredicate(format: "latitude <= %lf", myUserLatitude + 1.0)
let longitudeMinPredicate = NSPredicate(format: "longitude >= %lf", myUserLongitude - 1.0)
let longitudeMaxPredicate = NSPredicate(format: "longitude <= %lf", myUserLongitude + 1.0)

var compoundPredicate = NSCompoundPredicate()
compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [anotherUnrelatedPredicate,latitudeMinPredicate,latitudeMaxPredicate,longitudeMinPredicate, longitudeMaxPredicate])
fetchRequest.predicate = compoundPredicate        

Obviously I will create another property to scale the 1.0 value per the user desired region. Initial tests seem to work and best of all I can't believe how fast it it. Literally, the tableView is populated by the time the viewController segues to the enclosing mapView.

JohnSF
  • 3,736
  • 3
  • 36
  • 72
1

Well, yes it is possible. NSPredicate does have +predicateWithBlock: and init(block:).

However if you scroll down on that page you see the bad news:

Core Data supports block-based predicates in the in-memory and atomic stores, but not in the SQLite-based store.

So, whether you use an in-memory store, or do the filtering in code, either way you need to bring these 35,000 items into memory, and performance of a brute force approach will be poor.

There is a point of complexity at which SQL is no longer appropriate – you get better performance with real code. I think your requirement is far beyond that point. This will be an interesting computer science project in optimization. You need to do some pre-computing, analagous to adding an index to your database. Consider adding a region attribute to your place entities, then write your predicate to fetch all places within the target location's region and all immediate neighbors. Then filter through those candidates in code. I'm sure this has been done by others – think of cells in a cell phone network – but Core Data is not going to give you this for free.

Jerry Krinock
  • 4,860
  • 33
  • 39
  • Understood. I will look for ways to pre-condition. I originally took the region approach and segmented by state. That worked fine but of course if you want places within, say 100 miles and the user is in Reno, NV you would not get any hits for those a few miles away in CA. Thanks. – JohnSF Apr 02 '19 at 22:17
  • Yes, you got it. Your Reno/California example is why all immediate neighbors must be candidates. In a mathematically ideal world (with no mountains, water or cities) your regions would be shaped as hexagons, and each region would have six immediate neighbors. You are going to have fun with this :) – Jerry Krinock Apr 02 '19 at 23:52