Skip to content

Commit

Permalink
PartialKeyPath filtering on change notifications (#7411)
Browse files Browse the repository at this point in the history
  • Loading branch information
ejm01 authored Sep 2, 2021
1 parent 6ce85c3 commit 0adf050
Show file tree
Hide file tree
Showing 9 changed files with 1,053 additions and 1 deletion.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
x.y.z Release notes (yyyy-MM-dd)
=============================================================
### Enhancements
* None.
* Add additional `observe` methods for Objects and RealmCollections which take a `PartialKeyPath` type key path parameter.

### Fixed
* `Map<Key, Value>` did not conform to `Codable`. ([Cocoa #7418](https://github.com/realm/realm-cocoa/pull/7418), since v10.8.0)
Expand Down
135 changes: 135 additions & 0 deletions RealmSwift/LinkingObjects.swift
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,141 @@ import Realm
return rlmResults.addNotificationBlock(wrapObserveBlock(block), keyPaths: keyPaths, queue: queue)
}

/**
Registers a block to be called each time the collection changes.

The block will be asynchronously called with the initial results, and then called again after each write
transaction which changes either any of the objects in the collection, or which objects are in the collection.

The `change` parameter that is passed to the block reports, in the form of indices within the collection, which of
the objects were added, removed, or modified during each write transaction. See the `RealmCollectionChange`
documentation for more information on the change information supplied and an example of how to use it to update a
`UITableView`.

At the time when the block is called, the collection will be fully evaluated and up-to-date, and as long as you do
not perform a write transaction on the same thread or explicitly call `realm.refresh()`, accessing it will never
perform blocking work.

If no queue is given, notifications are delivered via the standard run loop, and so can't be delivered while the
run loop is blocked by other activity. If a queue is given, notifications are delivered to that queue instead. When
notifications can't be delivered instantly, multiple notifications may be coalesced into a single notification.
This can include the notification with the initial collection.

For example, the following code performs a write transaction immediately after adding the notification block, so
there is no opportunity for the initial notification to be delivered first. As a result, the initial notification
will reflect the state of the Realm after the write transaction.

```swift
class Person: Object {
@Persisted(originProperty: "handlers")
var dogs: LinkingObjects<Dog>
}
class Dog: Object {
@Persisted var name: String
@Persisted var handlers: List<Person>
}
// ...
let dogs = person.dogs
print("dogs.count: \(dogs?.count)") // => 0
let token = dogs.observe { changes in
switch changes {
case .initial(let dogs):
// Will print "dogs.count: 1"
print("dogs.count: \(dogs.count)")
break
case .update:
// Will not be hit in this example
break
case .error:
break
}
}
try! realm.write {
let dog = Dog()
dog.name = "Rex"
person.dogs.append(dog)
}
// end of run loop execution context
```

If no key paths are given, the block will be executed on any insertion,
modification, or deletion for all object properties and the properties of
any nested, linked objects. If a key path or key paths are provided,
then the block will be called for changes which occur only on the
provided key paths. For example, if:
```swift
class Person: Object {
@Persisted(originProperty: "handlers")
var dogs: LinkingObjects<Dog>
}
class Dog: Object {
@Persisted var name: String
@Persisted var age: Int
@Persisted var toys: List<Toy>
@Persisted var handlers: List<Person>
}
// ...
let dogs = person.dogs
let token = dogs.observe(keyPaths: [\Dog.name]) { changes in
switch changes {
case .initial(let dogs):
// ...
case .update:
// This case is hit:
// - after the token is intialized
// - when the name property of an object in the
// collection is modified
// - when an element is inserted or removed
// from the collection.
// This block is not triggered:
// - when a value other than name is modified on
// one of the elements.
case .error:
// ...
}
}
// end of run loop execution context
```
- If the observed key path were `[\Dog.toys.brand]`, then any insertion or
deletion to the `toys` list on any of the collection's elements would trigger the block.
Changes to the `brand` value on any `Toy` that is linked to a `Dog` in this
collection will trigger the block. Changes to a value other than `brand` on any `Toy` that
is linked to a `Dog` in this collection would not trigger the block.
Any insertion or removal to the `Dog` type collection being observed
would also trigger a notification.
- If the above example observed the `[\Dog.toys]` key path, then any insertion,
deletion, or modification to the `toys` list for any element in the collection
would trigger the block.
Changes to any value on any `Toy` that is linked to a `Dog` in this collection
would *not* trigger the block.
Any insertion or removal to the `Dog` type collection being observed
would still trigger a notification.

- note: Multiple notification tokens on the same object which filter for
separate key paths *do not* filter exclusively. If one key path
change is satisfied for one notification token, then all notification
token blocks for that object will execute.

You must retain the returned token for as long as you want updates to be sent to the block. To stop receiving
updates, call `invalidate()` on the token.

- warning: This method cannot be called during a write transaction, or when the containing Realm is read-only.

- parameter keyPaths: Only properties contained in the key paths array will trigger
the block when they are modified. If `nil`, notifications
will be delivered for any property change on the object.
See description above for more detail on linked properties.
- parameter queue: The serial dispatch queue to receive notification on. If
`nil`, notifications are delivered to the current thread.
- parameter block: The block to be called whenever a change occurs.
- returns: A token which must be held for as long as you want updates to be delivered.
*/
public func observe<T: ObjectBase>(keyPaths: [PartialKeyPath<T>],
on queue: DispatchQueue? = nil,
_ block: @escaping (RealmCollectionChange<LinkingObjects>) -> Void) -> NotificationToken {
return rlmResults.addNotificationBlock(wrapObserveBlock(block), keyPaths: keyPaths.map(_name(for:)), queue: queue)
}

// MARK: Frozen Objects

/// Returns if this collection is frozen.
Expand Down
128 changes: 128 additions & 0 deletions RealmSwift/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,134 @@ public final class List<Element: RealmCollectionValue>: RLMSwiftCollectionBase {
return rlmArray.addNotificationBlock(wrapObserveBlock(block), keyPaths: keyPaths, queue: queue)
}

/**
Registers a block to be called each time the collection changes.

The block will be asynchronously called with the initial results, and then called again after each write
transaction which changes either any of the objects in the collection, or which objects are in the collection.

The `change` parameter that is passed to the block reports, in the form of indices within the collection, which of
the objects were added, removed, or modified during each write transaction. See the `RealmCollectionChange`
documentation for more information on the change information supplied and an example of how to use it to update a
`UITableView`.

At the time when the block is called, the collection will be fully evaluated and up-to-date, and as long as you do
not perform a write transaction on the same thread or explicitly call `realm.refresh()`, accessing it will never
perform blocking work.

If no queue is given, notifications are delivered via the standard run loop, and so can't be delivered while the
run loop is blocked by other activity. If a queue is given, notifications are delivered to that queue instead. When
notifications can't be delivered instantly, multiple notifications may be coalesced into a single notification.
This can include the notification with the initial collection.

For example, the following code performs a write transaction immediately after adding the notification block, so
there is no opportunity for the initial notification to be delivered first. As a result, the initial notification
will reflect the state of the Realm after the write transaction.

```swift
class Person: Object {
@Persisted var dogs: List<Dog>
}
// ...
let dogs = person.dogs
print("dogs.count: \(dogs?.count)") // => 0
let token = dogs.observe { changes in
switch changes {
case .initial(let dogs):
// Will print "dogs.count: 1"
print("dogs.count: \(dogs.count)")
break
case .update:
// Will not be hit in this example
break
case .error:
break
}
}
try! realm.write {
let dog = Dog()
dog.name = "Rex"
person.dogs.append(dog)
}
// end of run loop execution context
```

If no key paths are given, the block will be executed on any insertion,
modification, or deletion for all object properties and the properties of
any nested, linked objects. If a key path or key paths are provided,
then the block will be called for changes which occur only on the
provided key paths. For example, if:
```swift
class Person: Object {
@Persisted var dogs: List<Dog>
}
class Dog: Object {
@Persisted var name: String
@Persisted var age: Int
@Persisted var toys: List<Toy>
}
// ...
let dogs = person.dogs
let token = dogs.observe(keyPaths: [\Dog.name]) { changes in
switch changes {
case .initial(let dogs):
// ...
case .update:
// This case is hit:
// - after the token is intialized
// - when the name property of an object in the
// collection is modified
// - when an element is inserted or removed
// from the collection.
// This block is not triggered:
// - when a value other than name is modified on
// one of the elements.
case .error:
// ...
}
}
// end of run loop execution context
```
- If the observed key path were `[\Dog.toys.brand]`, then any insertion or
deletion to the `toys` list on any of the collection's elements would trigger the block.
Changes to the `brand` value on any `Toy` that is linked to a `Dog` in this
collection will trigger the block. Changes to a value other than `brand` on any `Toy` that
is linked to a `Dog` in this collection would not trigger the block.
Any insertion or removal to the `Dog` type collection being observed
would also trigger a notification.
- If the above example observed the `[\Dog.toys]` key path, then any insertion,
deletion, or modification to the `toys` list for any element in the collection
would trigger the block.
Changes to any value on any `Toy` that is linked to a `Dog` in this collection
would *not* trigger the block.
Any insertion or removal to the `Dog` type collection being observed
would still trigger a notification.

- note: Multiple notification tokens on the same object which filter for
separate key paths *do not* filter exclusively. If one key path
change is satisfied for one notification token, then all notification
token blocks for that object will execute.

You must retain the returned token for as long as you want updates to be sent to the block. To stop receiving
updates, call `invalidate()` on the token.

- warning: This method cannot be called during a write transaction, or when the containing Realm is read-only.

- parameter keyPaths: Only properties contained in the key paths array will trigger
the block when they are modified. If `nil`, notifications
will be delivered for any property change on the object.
See description above for more detail on linked properties.
- parameter queue: The serial dispatch queue to receive notification on. If
`nil`, notifications are delivered to the current thread.
- parameter block: The block to be called whenever a change occurs.
- returns: A token which must be held for as long as you want updates to be delivered.
*/
public func observe<T: ObjectBase>(keyPaths: [PartialKeyPath<T>],
on queue: DispatchQueue? = nil,
_ block: @escaping (RealmCollectionChange<List>) -> Void) -> NotificationToken {
return rlmArray.addNotificationBlock(wrapObserveBlock(block), keyPaths: keyPaths.map(_name(for:)), queue: queue)
}

// MARK: Frozen Objects

public var isFrozen: Bool {
Expand Down
Loading

0 comments on commit 0adf050

Please sign in to comment.