That's very strange. I have a Model with three Entities. Like this:
In InterfaceBuilder I made NSArrayController connected to MOC via RepresentedObject to ViewController. Everything works, I can add and delete Master objects, select them, I can bind to TableView and edit them. But if I subclass NSArrayControler to MasterController and add just observer:
class MastersController: NSArrayController {
override func awakeFromNib() {
self.addObserver(self, forKeyPath: "selection", options: NSKeyValueObservingOptions.old, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
Swift.print("observing", keyPath ?? "<no path>")
switch keyPath! {
case "selection":
Swift.print("selection Changed")
default: break
}
}
TableView doesn't see already existing objects, only just added. I can edit them. But when I open the document again newly added objects disappear too. If I will change the class of controller back to NSArrayController I can see them all again.
Any help?
I'm almost sure observeValue(forKeyPath:of:change:context:) is used internally by NSArrayController and you should call super.observeValue(forKeyPath:of:change:context:) to get the expected behaviour...
The problem was solved by calling super.awakeFromNib() in overrided func awakeFromNib()
Related
I have an NSSearchField inside a NSToolbar that I am attempting to set makeFirstResponder on but it is working intermittently. At times the NSSearchField will become the first responder without the call to makeFirstResponder and makeFirstResponder is returning true as if it were set successfully. Setting NSWindow.initialFirstResponder has also failed to work.
class ViewController: NSViewController {
override func viewDidAppear() {
super.viewDidAppear()
view.window?.makeFirstResponder(view.window?.windowController?.searchField
}
}
I have had consistent working results by delaying the code with a timer but this is a less than ideal solution.
class ViewController: NSViewController {
override func viewDidAppear() {
super.viewDidAppear()
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
self.view.window?.makeFirstResponder(self.windowController?.searchField)
}
}
}
If makeFirstResponder is returning true, then the it likely was made the first responder for at least a short amount of time.
You can use the fact that NSWindow.firstResponder is KVO compliant in order to detect any changes to it with something like the following code in your ViewController class:
override func viewDidAppear() {
super.viewDidAppear()
self.view.window?.addObserver(self, forKeyPath: "firstResponder", options: [.initial, .new], context: nil)
self.view.window?.makeFirstResponder(self.windowController?.searchField)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "firstResponder" {
print("First responder of window: \(object) is \(change?[NSKeyValueChangeKey.newKey])")
}
}
I found a blog that led me to find the reason why this was happening. By default in macOS an NSWindow has an isRestorable Boolean value that will recall whatever the last firstResponder was regardless of what is set as an initialFirstResponder or what is set inside viewDidAppear, etc.
I found that calling webView.makeFirstResponder() didn't do anything.
But calling view.window?.makeFirstResponder(webView) did.
no idea why.
hours of frustration.
Of course, in a scene...
class DotScene: SKScene {
override func didMove(to view: SKView) {
print("this scene is now in place...")
you know the scene is in place when didMove#to is called.
(It's just like ViewDidAppear, you could say.)
However
I have no clue how to know that a sprite has been added to a scene.
class Spaceship: SKSpriteNode {
func wtf() {
this sprite has just been added to a scene
(such as with .childNode)
this sprite is now in place in the scene ...
There simply - has to be - a call that alerts you that a node has appeared on the scene successfully.
What is it ?
In SpriteKit there is no way to detect inside of a custom sprite class when a node is added to a scene. This is has been left out because you have control when a sprite is added to a scene via addChild or moveToParent.
Spritekit does not conform to an MVC architecture, where as UIKit does. One of the reasons didMoveToView exists is because the purpose of the View is to be an output display only. The controller is responsible for handling the code behind for the View. Now the view controller can be used to call presentScene from view, but if we are transitioning, we really have no idea at what point the scene is officially attached to the view. (There are other reasons besides this, I am just giving one example)
Now to get around this issue, you may be able to implement Key Value Observing (KVO), and listen to when scene gets set. To do this, simply do:
override init()
{
super.init()
addObserver(self, forKeyPath: #keyPath(SKNode.scene), options: [.old, .new, .initial], context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
{
if keyPath == #keyPath(SKNode.scene) && self.scene != nil
{
didAttachToScene()
}
}
func didAttachToScene()
{
}
deinit
{
removeObserver(self, forKeyPath: #keyPath(SKNode.scene))
}
Now, I have not tested this, I am currently in a windows environment, but if this does not work, you can listen to when a node is attached to a parent (which I have actually done with KVO so I know it is possible), and infer if we are on a scene that way since a node must have a parent to even qualify being on a scene.
override init()
{
super.init()
addObserver(self, forKeyPath: #keyPath(SKNode.parent), options: [.old, .new, .initial], context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
{
if keyPath == #keyPath(SKNode.parent) && self.scene != nil
{
didAttachToScene()
}
}
func didAttachToScene()
{
}
deinit
{
removeObserver(self, forKeyPath: #keyPath(self.parent))
}
This question already has answers here:
KVO broken in iOS 9.3
(3 answers)
Closed 5 years ago.
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
print("in viewDidLoad");
// addObserver keyPath
UserDefaults.standard.addObserver(self, forKeyPath: "testKey", options: .new, context: nil);
print("out viewDidLoad");
}
deinit {
// removeObserver keyPath
UserDefaults.standard.removeObserver(self, forKeyPath: "testKey");
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
print("in observeValue keyPath: \(keyPath) value: \(UserDefaults.standard.integer(forKey: "testKey"))");
// 1. If I execute the func click () method, it will be executed two times
// 2. If App originally existed "testKey", then func observeValue () will be executed after the viewDidLoad is finished.
}
#IBAction func click(_ sender: NSButton) {
UserDefaults.standard.set(arc4random(), forKey: "testKey");
}
}
The above code is all of my test code. I used KVO in my own project, but found repeated execution.
// 1. If I execute the func click () method, it will be executed two times
// 2. If App originally existed "testKey", then func observeValue () will be executed after the viewDidLoad is finished.
This is not what I understand about KVO. My idea is that after addObserver, my observeValue will be called if my key is changed. But it didn't turn out that way. I tried to find the answer to the forum, and I didn't find the answer. I just found a similar question.
If I press Button in my view, then the final result will be..:
in viewDidLoad
out viewDidLoad
in observeValue keyPath: Optional("testKey") value: 4112410111
in observeValue keyPath: Optional("testKey") value: 3712484288
in observeValue keyPath: Optional("testKey") value: 3712484288
macos: 10.12.6 (16G29)
xcode: 9 beta6、xcode 8.3.3
If you have the same problem, please tell more people to help us solve it. Thank you
I have sent the same question to the official, and if there is a solution, I will return it here.
From setting a breakpoint in observeValue() and looking at the trace, it appears that the observations are getting fired in two places; one during click() as an effect of the line where you tell UserDefaults to set the value, and another later on, scheduled on the run loop so it happens after click() has already returned, when the system detects that the value has changed. This double notification could probably be considered a bug, since the latter notification should render the former unnecessary, and I'd consider filing a radar report on it.
Unfortunately, I can't see any way to disable this behavior. I can think of a workaround, but it's extremely hacky, kludgey, ugly, and I probably wouldn't actually do it unless the need is absolutely dire. But here it is:
private var kvoContext = 0
private let ignoreKVOKey = "com.charlessoft.example.IgnoreKVO"
// If this can be called from a thread other than the main thread,
// then you will need to take measures to protect it against race conditions
private var shouldIgnoreKVO = false
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &self.kvoContext { // always, always use a context pointer
if !shouldIgnoreKVO { // if this is a notification we don't want, ignore it
print("in observeValue keyPath: \(String(describing: keyPath)) value: \(UserDefaults.standard.integer(forKey: "testKey"))");
}
} else {
// call super if context pointer doesn't match ours
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
#IBAction func click(_ sender: NSButton) {
// we don't need this notification, since we'll get the later one
// resulting from the defaults having changed
self.shouldIgnoreKVO = true
defer { self.shouldIgnoreKVO = false }
UserDefaults.standard.set(arc4random(), forKey: "testKey");
}
Again, it's ugly, it's hacky, I probably wouldn't actually do it. But there it is.
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
observing a property content of NSArrayController ... the first one
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?
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.
I have a NSCollectionView populated using binding via NSArrayController.
All i am trying is to add an observer to either NSArrayController or NSCollectionView so that when ever the selection changes i got a call a function.
For example this is what i am doing for NSTextField change :
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.textDidChange(_:)), name: NSTextViewDidChangeSelectionNotification, object: nil)
I am looking for the correct way to do it.
Able to achieve with this simple code :
Added this to applicationDidFinishLaunching :
self.fileListArrayController.addObserver(self, forKeyPath: "selectionIndexes", options: .New, context: nil)
And this function gets called every time user/program selects an item in my NSArrayController
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
print(keyPath)
}