响应变更 - Swift SDK
在此页面上
所有 Realm 对象均为活动对象,这表示在修改它们时会自动进行更新。每当发生任何属性变更时,Realm 均会发出通知事件。您可以注册一个通知处理程序来侦听这些通知事件,并使用最新数据更新您的用户界面。
本页介绍如何在 Swift 中手动注册通知侦听器。Atlas Device SDK for Swift 提供 SwiftUI 属性包装器,以便在发生数据变更时轻松自动更新用户界面。有关如何使用 SwiftUI 属性包装器响应变更的更多信息,请参阅观察对象。
注册 Realm 变更监听器
您可以在整个 Realm 上注册一个通知处理程序。每当提交涉及该 Realm 的任何写事务时,Realm 都会调用通知处理程序。处理程序不会接收到有关变更的信息。
RLMRealm *realm = [RLMRealm defaultRealm]; // Observe realm notifications. Keep a strong reference to the notification token // or the observation will stop. RLMNotificationToken *token = [realm addNotificationBlock:^(RLMNotification _Nonnull notification, RLMRealm * _Nonnull realm) { // `notification` is an enum specifying what kind of notification was emitted. // ... update UI ... }]; // ... // Later, explicitly stop observing. [token invalidate];
let realm = try! Realm() // Observe realm notifications. Keep a strong reference to the notification token // or the observation will stop. let token = realm.observe { notification, realm in // `notification` is an enum specifying what kind of notification was emitted viewController.updateUI() } // ... // Later, explicitly stop observing. token.invalidate()
注册集合变更监听器
您可以在 Realm 内的集合上注册通知处理程序。
Realm 会通知您的处理程序:
首次检索集合后。
每当写入事务添加、变更或删除集合中的对象时。
通知使用三个索引列表描述自先前通知以来的变更:已删除对象、已插入对象和已修改对象的索引。
重要
顺序很重要
在集合通知处理程序中,始终按以下顺序应用变更:删除、插入和修改。在删除操作之前处理插入操作可能会导致意外行为。
集合通知提供了一个 change
参数,用于报告在写事务(write transaction)期间删除、添加或修改了哪些对象。 此 RealmCollectionChange解析为一个索引路径大量,您可以将其传递给UITableView
的批处理更新方法。
重要
高频更新
此集合变更侦听器示例不支持高频更新。在高强度的工作负载下,此集合变更侦听器可能会导致应用程序抛出异常。
@interface CollectionNotificationExampleViewController : UITableViewController @end @implementation CollectionNotificationExampleViewController { RLMNotificationToken *_notificationToken; } - (void)viewDidLoad { [super viewDidLoad]; // Observe RLMResults Notifications __weak typeof(self) weakSelf = self; _notificationToken = [[Dog objectsWhere:@"age > 5"] addNotificationBlock:^(RLMResults<Dog *> *results, RLMCollectionChange *changes, NSError *error) { if (error) { NSLog(@"Failed to open realm on background worker: %@", error); return; } UITableView *tableView = weakSelf.tableView; // Initial run of the query will pass nil for the change information if (!changes) { [tableView reloadData]; return; } // Query results have changed, so apply them to the UITableView [tableView performBatchUpdates:^{ // Always apply updates in the following order: deletions, insertions, then modifications. // Handling insertions before deletions may result in unexpected behavior. [tableView deleteRowsAtIndexPaths:[changes deletionsInSection:0] withRowAnimation:UITableViewRowAnimationAutomatic]; [tableView insertRowsAtIndexPaths:[changes insertionsInSection:0] withRowAnimation:UITableViewRowAnimationAutomatic]; [tableView reloadRowsAtIndexPaths:[changes modificationsInSection:0] withRowAnimation:UITableViewRowAnimationAutomatic]; } completion:^(BOOL finished) { // ... }]; }]; } @end
class CollectionNotificationExampleViewController: UITableViewController { var notificationToken: NotificationToken? override func viewDidLoad() { super.viewDidLoad() let realm = try! Realm() let results = realm.objects(Dog.self) // Observe collection notifications. Keep a strong // reference to the notification token or the // observation will stop. notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in guard let tableView = self?.tableView else { return } switch changes { case .initial: // Results are now populated and can be accessed without blocking the UI tableView.reloadData() case .update(_, let deletions, let insertions, let modifications): // Query results have changed, so apply them to the UITableView tableView.performBatchUpdates({ // Always apply updates in the following order: deletions, insertions, then modifications. // Handling insertions before deletions may result in unexpected behavior. tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), with: .automatic) tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), with: .automatic) tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), with: .automatic) }, completion: { finished in // ... }) case .error(let error): // An error occurred while opening the Realm file on the background worker thread fatalError("\(error)") } } } }
注册对象变更侦听器
您可以在 Realm 中的特定对象上注册通知处理程序。Realm 会通知您的处理程序:
当删除对象时。
当对象的任何属性发生变更时。
处理程序接收有关哪些字段已变更以及对象是否已被删除的信息。
@interface Dog : RLMObject @property NSString *name; @property int age; @end @implementation Dog @end RLMNotificationToken *objectNotificationToken = nil; void objectNotificationExample() { Dog *dog = [[Dog alloc] init]; dog.name = @"Max"; dog.age = 3; // Open the default realm RLMRealm *realm = [RLMRealm defaultRealm]; [realm transactionWithBlock:^{ [realm addObject:dog]; }]; // Observe object notifications. Keep a strong reference to the notification token // or the observation will stop. Invalidate the token when done observing. objectNotificationToken = [dog addNotificationBlock:^(BOOL deleted, NSArray<RLMPropertyChange *> * _Nullable changes, NSError * _Nullable error) { if (error != nil) { NSLog(@"An error occurred: %@", [error localizedDescription]); return; } if (deleted) { NSLog(@"The object was deleted."); return; } NSLog(@"Property %@ changed to '%@'", changes[0].name, changes[0].value); }]; // Now update to trigger the notification [realm transactionWithBlock:^{ dog.name = @"Wolfie"; }]; }
// Define the dog class. class Dog: Object { var name = "" } var objectNotificationToken: NotificationToken? func objectNotificationExample() { let dog = Dog() dog.name = "Max" // Open the default realm. let realm = try! Realm() try! realm.write { realm.add(dog) } // Observe object notifications. Keep a strong reference to the notification token // or the observation will stop. Invalidate the token when done observing. objectNotificationToken = dog.observe { change in switch change { case .change(let object, let properties): for property in properties { print("Property '\(property.name)' of object \(object) changed to '\(property.newValue!)'") } case .error(let error): print("An error occurred: \(error)") case .deleted: print("The object was deleted.") } } // Now update to trigger the notification try! realm.write { dog.name = "Wolfie" } }
注册键路径变更侦听器
版本 10.12.0 新增。
除了在对象或集合上注册通知处理程序之外,您还可以传递可选的string keyPaths
参数来指定要监视的一个或多个关键路径。
例子
// Define the dog class. class Dog: Object { var name = "" var favoriteToy = "" var age: Int? } var objectNotificationToken: NotificationToken? func objectNotificationExample() { let dog = Dog() dog.name = "Max" dog.favoriteToy = "Ball" dog.age = 2 // Open the default realm. let realm = try! Realm() try! realm.write { realm.add(dog) } // Observe notifications on some of the object's key paths. Keep a strong // reference to the notification token or the observation will stop. // Invalidate the token when done observing. objectNotificationToken = dog.observe(keyPaths: ["favoriteToy", "age"], { change in switch change { case .change(let object, let properties): for property in properties { print("Property '\(property.name)' of object \(object) changed to '\(property.newValue!)'") } case .error(let error): print("An error occurred: \(error)") case .deleted: print("The object was deleted.") } }) // Now update to trigger the notification try! realm.write { dog.favoriteToy = "Frisbee" } // When you specify one or more key paths, changes to other properties // do not trigger notifications. In this example, changing the "name" // property does not trigger a notification. try! realm.write { dog.name = "Maxamillion" } }
版本 10.14.0 中的新增功能。
您可以 观察 到部分类型擦除的 PartialKeyPath 对象 或 RealmCollections 上。
objectNotificationToken = dog.observe(keyPaths: [\Dog.favoriteToy, \Dog.age], { change in
当您指定 keyPaths
时,只有对 keyPaths
的变更才会触发通知块。任何其他变更都不会触发通知块。
例子
考虑一个 Dog
对象,其属性之一是一个 siblings
列表:
class Dog: Object { var name = "" var siblings: List<Dog> var age: Int? }
如果您将 siblings
作为 keyPath
传递以进行观察,则对 siblings
列表的任何插入、删除或修改都会触发通知。但是,除非您明确观察到了 ["siblings.name"]
,否则对 someSibling.name
的变更不会触发通知。
注意
同一对象上的多个通知令牌(针对单独的键路径进行过滤)不会进行排他性过滤。如果一项键路径变更符合一个通知令牌的条件,则该对象的所有通知令牌块都将执行。
Realm 集合
当您观察各种集合类型的键路径时,预计会出现以下行为:
LinkingObjects::观察 LinkingObject 的某个属性会触发该属性变更的通知,但不会触发该对象的其他属性变更的通知。针对列表或列表所在对象的插入或删除操作会触发通知。
列表:观察列表对象的某个属性会针对该属性的变更Atlas Triggers通知,但不会针对其他属性的变更trigger通知。 对列表或列表所在对象的插入或删除操作会trigger通知。
地图:观察地图对象Atlas Triggers的某一属性,该属性发生变更时会触发通知,但其他属性发生变更时不会trigger通知。 对 Map 或映射所在对象的插入或删除操作会trigger通知。
change
参数以映射中键的形式报告在每个写事务期间添加、删除或修改了哪些键值对。MutableSet :观察 MutableSet对象的某个属性会触发该属性更改的通知,但不会trigger其他属性更改的通知。 对 MutableSet 或 MutableSet 所在对象的插入或删除操作会trigger通知。
结果:观察结果的属性会触发该属性更改的通知,但不会trigger其他属性更改的通知。 对 Result 的插入或删除操作会trigger通知。
静默写入
您可以在不向特定观察者发送通知的情况下写入域,具体方法是将数组中的观察者的通知令牌传递给 realm.write(withoutNotifying:_:):
RLMRealm *realm = [RLMRealm defaultRealm]; // Observe realm notifications RLMNotificationToken *token = [realm addNotificationBlock:^(RLMNotification _Nonnull notification, RLMRealm * _Nonnull realm) { // ... handle update }]; // Later, pass the token in an array to the realm's `-transactionWithoutNotifying:block:` method. // Realm will _not_ notify the handler after this write. [realm transactionWithoutNotifying:@[token] block:^{ // ... write to realm }]; // Finally [token invalidate];
let realm = try! Realm() // Observe realm notifications let token = realm.observe { notification, realm in // ... handle update } // Later, pass the token in an array to the realm.write(withoutNotifying:) // method to write without sending a notification to that observer. try! realm.write(withoutNotifying: [token]) { // ... write to realm } // Finally token.invalidate()
停止关注变更
当 observe
调用返回的令牌失效时,观察便会停止。您可以通过调用其 invalidate()
方法来显式使其失效。
重要
只要您想观察,就保留令牌
如果令牌存储在超出作用域的局部变量中,则通知将停止。
RLMRealm *realm = [RLMRealm defaultRealm]; // Observe and obtain token RLMNotificationToken *token = [realm addNotificationBlock:^(RLMNotification _Nonnull notification, RLMRealm * _Nonnull realm) { /* ... */ }]; // Stop observing [token invalidate];
let realm = try! Realm() // Observe and obtain token let token = realm.observe { notification, realm in /* ... */ } // Stop observing token.invalidate()
键值观察
键值观察合规性
Realm 对象的大多数属性都符合键值观察 (KVO) 标准 :
Object
子类上的几乎所有托管(非忽略)属性Object
和List
上的invalidated
属性
您无法通过键值观察来观察 LinkingObjects
属性。
重要
当对象具有任何已注册的观察者时,您无法将该对象添加到 Realm(使用 realm.add(obj)
或类似方法)。
托管与非托管 KVO 注意事项
观察 Object
子类的非托管实例的属性就像观察任何其他动态属性一样。
观察托管对象的属性的工作方式有所不同。对于 Realm 托管的对象,属性的值可能会在以下情况中发生变化:
您对其进行赋值
Realm 进行刷新,可以是使用
realm.refresh()
手动刷新,也可以是在 runloop 线程上自动刷新在另一个线程上发生变更后开始写事务
Realm 会一次性将写事务中所做的变更应用到其他线程上。观察者会立即看到键值观察通知。中间步骤不会触发 KVO 通知。
例子
假设您的应用执行的写事务将属性从 1 递增到 10。在主线程上,您会直接收到从 1 变更到 10 的单个通知。您不会收到从 1 到 10 之间每次递增变更的通知。
请避免在 observeValueForKeyPath(_:ofObject:change:context:)
中修改托管 Realm 对象。当不在写事务中,或作为开始写事务的一部分时,属性值可能会发生变更。
观察 Realm 列表
观察针对 Realm List
属性的变更要比观察对 NSMutableArray
属性的变更更简单:
您不必将
List
属性标记为动态即可观察它们。您可以直接调用
List
上的修改方法。观察对其进行存储的属性的任何人都会收到通知。
您不需要使用 mutableArrayValueForKey(_:)
,尽管为了保持代码兼容性,Realm 支持此操作。
在不同执行者上响应变更
您可以在不同执行者上观察通知。调用 await object.observe(on: Actor)
或 await collection.observe(on: Actor)
会注册一个块,以便在每次对象或集合发生变更时进行调用。
// Create a simple actor actor BackgroundActor { public func deleteTodo(tsrToTodo tsr: ThreadSafeReference<Todo>) throws { let realm = try! Realm() try realm.write { // Resolve the thread safe reference on the Actor where you want to use it. // Then, do something with the object. let todoOnActor = realm.resolve(tsr) realm.delete(todoOnActor!) } } } // Execute some code on a different actor - in this case, the MainActor func mainThreadFunction() async throws { let backgroundActor = BackgroundActor() let realm = try! await Realm() // Create a todo item so there is something to observe try await realm.asyncWrite { realm.create(Todo.self, value: [ "_id": ObjectId.generate(), "name": "Arrive safely in Bree", "owner": "Merry", "status": "In Progress" ]) } // Get the collection of todos on the current actor let todoCollection = realm.objects(Todo.self) // Register a notification token, providing the actor where you want to observe changes. // This is only required if you want to observe on a different actor. let token = await todoCollection.observe(on: backgroundActor, { actor, changes in print("A change occurred on actor: \(actor)") switch changes { case .initial: print("The initial value of the changed object was: \(changes)") case .update(_, let deletions, let insertions, let modifications): if !deletions.isEmpty { print("An object was deleted: \(changes)") } else if !insertions.isEmpty { print("An object was inserted: \(changes)") } else if !modifications.isEmpty { print("An object was modified: \(changes)") } case .error(let error): print("An error occurred: \(error.localizedDescription)") } }) // Update an object to trigger the notification. // This example triggers a notification that the object is deleted. // We can pass a thread-safe reference to an object to update it on a different actor. let todo = todoCollection.where { $0.name == "Arrive safely in Bree" }.first! let threadSafeReferenceToTodo = ThreadSafeReference(to: todo) try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) // Invalidate the token when done observing token.invalidate() }
有关对其他 Actor 的变更通知的更多信息,请参阅观察对其他 Actor 的通知。
响应对类投影的变更
与其他域对象一样,你可以对类投影的变更做出响应。当您注册类投影变更侦听器时,您可以直接看到通过类投影对象的变更的通知。您还可以看到通过类投影对象投影的底层对象属性变更的通知。
类投影中未 @Projected
的底层对象的属性不会生成通知。
此通知块会针对以下方面的变更而触发:
Person.firstName
类投影的底层Person
对象的属性,但不会变更为Person.lastName
或Person.friends
。PersonProjection.firstName
属性,但不是另一个使用相同底层对象属性的类投影。
let realm = try! Realm() let projectedPerson = realm.objects(PersonProjection.self).first(where: { $0.firstName == "Jason" })! let token = projectedPerson.observe(keyPaths: ["firstName"], { change in switch change { case .change(let object, let properties): for property in properties { print("Property '\(property.name)' of object \(object) changed to '\(property.newValue!)'") } case .error(let error): print("An error occurred: \(error)") case .deleted: print("The object was deleted.") } }) // Now update to trigger the notification try! realm.write { projectedPerson.firstName = "David" }
通知传递
通知传递可能因以下因素而异:
通知是否发生在写事务中
写入和观察的相对线索
当您的应用程序依赖于通知传送的时间时(例如,当您使用通知来更新 UITableView
时),了解应用程序代码上下文的特定行为非常重要。
在观察线程之外的不同线程上执行写入
从变更通知内部读取观察到的集合或对象始终可以准确地告诉您自上次调用回调以来传递给回调的集合中发生了哪些变更。
读取变更通知以外的集合或对象始终会提供与您在该对象的最新变更通知中看到的完全相同的值。
在发送该变更的通知之前,读取变更通知中观察到的对象以外的对象可能会看到不同的值。Realm refresh
通过一次操作将整个 Realm 从“旧版本”转变为“最新版本”。但是,“旧版本”和“最新版本”之间可能会触发多个变更通知。在回调中,您可能会看到具有待处理通知的变更。
不同线程上的写入最终将在观察线程上变为可见。显式调用 refresh()
块,直到其他线程上的写入可见并且已发送相应的通知。如果您在通知回调中调用 refresh()
,则这是一个空操作。
在通知外部的观察线程上执行写入
在写事务开始时,上述所有行为都适用于此上下文。此外,您可以期望始终看到最新版本的数据。
在写事务中,您看到的唯一变更是目前为止在写事务中所做的变更。
在提交写事务和发送下一组变更通知之间,您可以看到在写事务中所做的变更,但看不到其他变更。在收到下一组通知之前,在不同线程上进行的写入不会变为可见。在同一线程上执行另一项写入会首先发送上一次写入的通知。
在通知内部执行写入
当您在通知中执行写入时,您会看到许多与上面相同的行为,但有一些例外情况。
在执行写入操作之前调用的回调行为正常。虽然 Realm 以稳定的顺序调用变更回调,但严格来说,这并不是你添加观察的顺序。
如果开始写入时会刷新 Realm(如果另一个线程正在进行写入,则可能会发生这种情况),这会触发递归通知。这些嵌套通知会报告自上次调用回调以来所进行的变更。对于写入操作之前发生的回调,这意味着内部通知仅会报告在外部通知中已报告的变更之后所进行的变更。如果进行写入的回调尝试在内部通知中再次进行写入,Realm 会抛出异常。写入操作后发生的回调会同时收到两组变更的通知。
当回调完成写入并返回后,Realm 不会调用任何后续回调,因为不再有任何变更需要报告。Realm 稍后会提供关于写入的通知,就好像写入是发生在通知之外一样。
如果开始写入时没有刷新 Realm,则写入会照常进行。但是,Realm 会在不一致的状态下调用后续回调。它们继续报告原始变更信息,但观察到的对象/集合现在包含先前回调中进行的写入变更。
如果您尝试执行手动检查和写入处理以从写事务中获取更细粒度的通知,则可以获得嵌套深度超过两层的通知。手动写入处理的一个示例是检查 realm.isInWriteTransaction
,如果需要进行更改,则依次调用 realm.commitWrite()
和 realm.beginWrite()
。多个嵌套的通知和潜在错误使得这种手动操作易于出错并且难以调试。
如果不需要来自写入块内部的细粒度变更信息,则可以使用 writeAsync API 来简化处理。通过观察与此相似的异步写入,即使通知在写事务中传递,系统也会提供通知:
let token = dog.observe(keyPaths: [\Dog.age]) { change in guard case let .change(dog, _) = change else { return } dog.realm!.writeAsync { dog.isPuppy = dog.age < 2 } }
但是,由于写入是异步的,因此在通知和写入发生之间,Realm 可能已发生变更。在这种情况下,传递至通知的变更信息可能不再适用。
根据通知更新 UITableView
如果您仅通过通知来更新 UITableView
,则在写事务和下一个通知到达之间的时间内,TableView 的状态与数据不同步。TableView 可能有一个待处理的计划内更新,这可能会导致更新延迟或不一致。
您可以通过几种方式来处理这些行为。
以下示例使用这个非常基本的 UITableViewController
。
class TableViewController: UITableViewController { let realm = try! Realm() let results = try! Realm().objects(DemoObject.self).sorted(byKeyPath: "date") var notificationToken: NotificationToken! override func viewDidLoad() { super.viewDidLoad() tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") notificationToken = results.observe { (changes: RealmCollectionChange) in switch changes { case .initial: self.tableView.reloadData() case .update(_, let deletions, let insertions, let modifications): // Always apply updates in the following order: deletions, insertions, then modifications. // Handling insertions before deletions may result in unexpected behavior. self.tableView.beginUpdates() self.tableView.deleteRows(at: deletions.map { IndexPath(row: $0, section: 0) }, with: .automatic) self.tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) }, with: .automatic) self.tableView.reloadRows(at: modifications.map { IndexPath(row: $0, section: 0) }, with: .automatic) self.tableView.endUpdates() case .error(let err): fatalError("\(err)") } } } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return results.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let object = results[indexPath.row] let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = object.title return cell } func delete(at index: Int) throws { try realm.write { realm.delete(results[index]) } } }
在没有通知的情况下直接更新 UITableView
无需等待通知即可直接更新 UITableView
,从而提供响应速度最快的用户界面。此代码会立即更新 TableView,而不需要在线程之间进行跳转(这会为每次更新增加少量延迟)。缺点是需要经常手动更新视图。
func delete(at index: Int) throws { try realm.write(withoutNotifying: [notificationToken]) { realm.delete(results[index]) } tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) }
写入后强制执行刷新
在写入后强制执行 refresh()
可立即提供来自写入的通知,而不是在未来运行事件循环时提供通知。TableView 没有可用于读取非同步值的窗口。
缺点是,这意味着我们建议在后台执行的操作(例如写入、重新运行查询和对结果重新排序)会在主线程上进行。如果这些操作的计算量很大,可能会导致主线程延迟。
func delete(at index: Int) throws { try realm.write { realm.delete(results[index]) } realm.refresh() }
在后台线程上执行写入
对后台线程执行写入操作可以尽量减少主线程被阻塞的时间。然而,对后台执行写入的代码要求更熟悉 Realm 的线程模型以及 Swift DispatchQueue 的用法。由于写入操作未发生在主线程上,因此在通知到达之前,主线程永不会看到该写入操作。
func delete(at index: Int) throws { func delete(at index: Int) throws { var object = results[index] DispatchQueue.global().async { guard let object = object else { return } let realm = object.realm! try! realm.write { if !object.isInvalidated { realm.delete(object) } } } } }
变更通知限制
嵌套文档中深度超过四级的变更不会触发变更通知。
如果您的数据结构需要侦听第五层深度或更深层的更改,解决方法包括:
重构模式以减少嵌套。
添加“推送以刷新”一类的内容,使用户能够手动刷新数据。
在 Swift SDK 中,您还可以使用键路径过滤来解决这一限制。此功能在其他 SDK 中不可用。