Could you help me how to manage to be notified when the contents of NSArrayController
are modified, using Smart KeyPaths?
Inspired by
Key-Value Observing: https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html#//apple_ref/doc/uid/TP40014216-CH7-ID12
Smart KeyPaths: Better Key-Value Coding for Swift: https://github.com/apple/swift-evolution/blob/master/proposals/0161-key-paths.md
I mimicked the example code of the article.
class myArrayController: NSArrayController {
required init?(coder: NSCoder) {
super.init(coder: coder)
observe(\.content, options: [.new]) { object, change in
print("Observed a change to \(object.content.debugDescription)")
}
}
}
However, that is not working. Any changes made on the target object does not fire notification.
In contrast, the typical way listed below is working.
class myArrayController: NSArrayController {
required init?(coder: NSCoder) {
super.init(coder: coder)
addObserver(self, forKeyPath: "content", options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "content" {
print("Observed a change to \((object as! myArrayController).content.debugDescription)")
}
else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}
The new way looks more elegant. Any your suggestions?
Environment: Xcode 9 Beta
Use Core Data
myArrayController
's Mode is Entity Name, prepared with Document.xcdatamodeld
myArrayController
's Managed Object Context is bound to Model Key Path: representedObject.managedObjectContext
representedObject
is assigned with the instance of Document
.NSTableView
's Content, Selection Indexes, and Sort Descriptors are bound to the correspondences of the myArrayController
.More information on the environment: Binding managedObjectContext, Xcode 8.3.2, Storyboards, mac: https://forums.bignerdranch.com/t/binding-managedobjectcontext-xcode-8-3-2-storyboards-macos-swift/12284
EDITED:
Regarding the example case cited above, I have changed my mind to observe managedObjectContext
, instead of content
of NSArrayController
.
class myViewController: NSViewController {
override func viewWillAppear() {
super.viewWillAppear()
let n = NotificationCenter.default
n.addObserver(self, selector: #selector(mocDidChange(notification:)),
name: NSNotification.Name.NSManagedObjectContextObjectsDidChange,
object: (representedObject as! Document).managedObjectContext)
}
}
@objc func mocDidChange(notification n: Notification) {
print("\nmocDidChange():\n\(n)")
}
}
The reason is that this second approach is simpler than the first one. This code covers all of the desired requirements: additions and deletions of table rows, and modifications of table cells' value. The drawback is that every another table's modification and every yet another entities' modification within the App will cause notifications. Such a notification is not interesting, though. However, that is not a big deal.
In contrast, the first approach will require more complexity.
For additions and deletions, we would need either observing content
of NSArrayController
or implementing two functions
func tableView(_ tableView: NSTableView, didAdd rowView: NSTableRowView, forRow row: Int)
func tableView(_ tableView: NSTableView, didRemove rowView: NSTableRowView, forRow row: Int)
from NSTableViewDelegate
. NSTableView
's delegate
is connected to NSViewController
.
Slightly surprisingly, the both tableView()
functions will be called so frequently. For instance, in the situation where there are ten rows in a table, sorting rows will result in ten didRemove
calls followed by ten didAdd
calls; adding one row will result in ten didRemove
calls and then eleven didAdd
calls. That is not so efficient.
For modifications, we would need
func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool
from NSControlTextEditingDelegate
, a super of NSTableViewDelegate
. Every NSTextField
of each table column should be connected to NSViewController
via its delegate
.
Furthermore, unfortunately, this control()
is called right after text edition is completed, but rather, before the actual value in the NSArrayController
has been updated. That is, somewhat, useless. I have not yet found good solution with the first approach.
ANYWAY, the primary topic in this post is how to use Smart KeyPaths. :-)
EDITED 2:
I am going to use both
content
of NSArrayController
... the first oneNotification
being posted by NSManagedObjectContext
... the second oneThe 1 is for when a user changes master-details view, which does not make a change on NSManagedObjectContext
.
The 2 is for when a user makes a change on it: addition, removal, updating, as well as undo, Command-Z, which is not accompanied by mouse events.
For now, the version of addObserver(self, forKeyPath: "content", ...
will be used. Once the question of this post has been solved, I will switch to the version of observe(\.content, ...
Thanks.
EDITED 3:
The code 2. observing a Notification
has been completely replaced with new one.
As for your initial code, here's what it should look like:
class myArrayController: NSArrayController {
private var mySub: Any? = nil
required init?(coder: NSCoder) {
super.init(coder: coder)
self.mySub = self.observe(\.content, options: [.new]) { object, change in
debugPrint("Observed a change to", object.content)
}
}
}
The observe(...)
function returns a transient observer whose lifetime indicates how long you'll receive notifications for. If the returned observer is deinit
'd, you will no longer receive notifications. In your case, you never retained the object so it died right after the method scope.
In addition, to manually stop observing, just set mySub
to nil
, which implicitly deinit
s the old observer object.