Search code examples
swiftdynamicrealmobservers

Limit objects in Realm Results by condition in order to observe result


I want to limit the objects in a realm results by some predicate in order to observe this results and update my UI based on its changes.

I have seen several solutions arguing that because objects in realm are lazy-loaded there is no need to limit the query. However I would like the query to constrain results by the limit in order for change notifications to fire when the objects constrained by the limit changes.

Let me give you an example:

Suppose I have a package object:

class Package: Object {

    @objc enum State: Int {
        case scheduled
        case delivered
    }

    @objc dynamic var state = State.scheduled
    @objc dynamic var deliveryDate = Date()
}

and I want to create a results of all packages that are scheduled AND ONLY the newest package that is delivered. How would I go about doing that? Is this even possible with a single Results that I could then observe for changes?

UPDATE:

Trying to be more clear about what I am looking for. Lets say I have the following packages in my realm:

Package1
  state: delivered
  deliveryDate: March 1st

Package2
  state: delivered
  deliveryDate: March 2nd

Package3
  state: delivered
  deliveryDate: March 3rd

Package4
  state: scheduled
  deliveryDate: March 4th

Package5
  state: scheduled
  deliveryDate: March 5th

Package6
  state: scheduled
  deliveryDate: March 6th

and let's say that these packages change state on a daily basis. So on March 3rd the data would look as above, but on March 4th Package4 would have changed state to 'delivered'. I then want a results that would reflect the following on those two days:

March 3rd:
  - Package3 (delivered)
  - Package4 (scheduled)
  - Package5 (scheduled)
  - Package6 (scheduled)

March 4th:
  - Package4 (delivered)
  - Package5 (scheduled)
  - Package6 (scheduled)

Solution

  • I going to attempt an answer but it may be off base.

    The objective is to have a single realm result that can be observed for changes and also know what the newest delivery is.

    Here's the code

    self.packageResults = realm.objects(Package.self).sorted(byKeyPath: "delivered", ascending: false)
    

    Then observe self.packageResults.

    Whenever there's a change the observe closure will fire and deliver the results, sorted descending so the most current delivery will be 'at the top' i.e. index 0.

    EDIT

    Based on a comment and update to the question there was a question whether this answer provides a solution. Now there is a bit more data, just a slight modification produces the desired result (almost, keep reading)

    self.packageResults = realm.objects(Package.self)
                               .filter("deliveryDate => 20190303")
                               .sorted(byKeyPath: "state", ascending: false)
    

    then add an observer to self.packageResults. (note that I am using an Int for the timestamp to keep it simple)

    To test, we use the data provided in the question with our observer code. The code prints the initial state of the data, and then prints the state after any delete, insert or modify.

    func doObserve() {
         self.notificationToken = self.packageResults!.observe { (changes: RealmCollectionChange) in
             switch changes {
             case .initial:
                 print("initial load complete")
                 if let results = self.packageResults {
                     for p in results {
                         print(p.deliveryDate, p.state.rawValue)
                     }
                 }
                 break
             case .update(_, let deletions, let insertions, let modifications):
                 if let results = self.packageResults {
                     for p in results {
                         print(p.deliveryDate, p.state.rawValue)
                     }
                 }
    

    then we run the code and add the observer. This is the output

    initial load complete
    20190303 1
    20190304 0
    20190305 0
    20190306 0
    

    if we then change the package 20190304 to 'delivered' we get the following output

    20190303 1
    20190304 1
    20190305 0
    20190306 0
    

    So we are getting the desired result. HOWEVER, according to the question, the conditions of the query change as the next query would be for dates 20190304 and after.

    If the conditions of the output change, the conditions of the query would have to change as well. In this case the desired result is to display the most currently delivered package each day, so the query would need to be updated each day to reflect that date.

    As a possible solution, change the Package object to include a watched property

    class Package: Object {
        @objc enum State: Int {
            case scheduled
            case delivered
        }
        @objc dynamic var state = State.scheduled
        @objc dynamic var deliveryDate = 0
        @objc dynamic var watched = false
    }
    

    and update the query to only observe packages being watched, ignore ones not being watched (where watched = false)

    self.packageResults = realm.objects(Package.self)
                               .filter("deliveryDate => 20190303 and watched == true")
                               .sorted(byKeyPath: "state", ascending: false)
    

    Then, when you no longer need to know about an object, set it's watched property to false, and it will not be included in the results. Using the same process as above with this query, when changing 20190304 to delivered, we also change 20190303 watched property to false and the results are

     handle item delete, insert or mod
    20190304 1
    20190305 0
    20190306 0
    

    which appears to answer the question.