Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use Key-Value Observing with Smart KeyPaths in Swift 4?

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

  • macOS, Cocoa App, Swift 4
  • Create Document-Based Application
  • 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

  1. observing a property content of NSArrayController ... the first one
  2. observing a Notification being posted by NSManagedObjectContext ... the second one

The 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.

  • How to notice changes on Managed Object Entity handled by Core Data?
like image 698
Tora Avatar asked Jun 29 '17 14:06

Tora


People also ask

What is key-value observing Swift?

Key-value observing is a Cocoa programming pattern you use to notify objects about changes to properties of other objects. It's useful for communicating changes between logically separated parts of your app—such as between models and views.

What framework is KVO key-value observing a part of?

Key-Value Observing, KVO for short, is an important concept of the Cocoa API. It allows objects to be notified when the state of another object changes.

What is Property observation in Swift?

Property observers observe and respond to changes in a property's value. Property observers are called every time a property's value is set, even if the new value is the same as the property's current value. You can add property observers in the following places: Stored properties that you define.

What is a KeyPath in Swift?

KeyPath is a type that represent a references to properties and subscripts. Since it is a type, you can store it in a variable, pass it around, or even perform an operation on a key path. We can use a key path to get/set their underlying values at a later time.


1 Answers

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 deinits the old observer object.

like image 109
Aditya Vaidyam Avatar answered Oct 02 '22 15:10

Aditya Vaidyam