MacOS uses KVO to perform multiple executions [duplicate] - swift

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.

Related

window.windowController is nil inside windowWillClose() but it isn't inside viewDidAppear()

I've tried, without success, respond to events such as windowWillClose() and windowShouldClose() inside NSWindowController (yes conforming to NSWindowDelegate).
Later, to my surprise, I was able to receive those events if I make my contentViewController (NSViewController) conform to NSWindowDelegate.
Unfortunately, later on, found out that view.window?.windowController is nil inside windowWillClose() or windowShouldClose(), code:
override func viewDidAppear() {
super.viewDidAppear()
self.view.window?.delegate = self
self.view.window?.windowController // not nil!
}
func windowWillClose(_ notification: Notification) {
self.view.window?.windowController // nil!!
}
func windowShouldClose(_ sender: NSWindow) -> Bool {
self.view.window?.windowController // nil!!
return true
}
After realizing that view.window?.windowController is not nil inside viewDidAppear() the next thing I thought was that Swift garbage collected the controller, so I changed viewDidAppear() in a way that creates another reference of windowController thus preventing garbage collection on said object, code:
var windowController: NSWindowController?
override func viewDidAppear() {
super.viewDidAppear()
self.view.window?.delegate = self
windowController = view.window?.windowController
}
func windowWillClose(_ notification: Notification) {
self.view.window?.windowController // NOT nil
}
func windowShouldClose(_ sender: NSWindow) -> Bool {
self.view.window?.windowController // NOT nil
return true
}
My hypothesis turned out to be correct (I think).
Is this the same issue that is preventing me from receiving those events inside NSWindowController?
Is there another way I can achieve the same thing without creating more object references?
In order to post code, I use the Answer option even though it is more of a comment.
I added in NSViewController:
override func viewDidAppear() {
super.viewDidAppear()
parentWindowController = self.view.window!.windowController
self.view.window!.delegate = self.view.window!.windowController as! S1W2WC. // The NSWC class, which conforms to NSWindowDelegate
print(#function, "windowController", self.view.window!, self.view.window!.windowController)
}
I get print log:
viewDidAppear() windowController Optional()
and notification is passed.
But if I change to
override func viewDidAppear() {
super.viewDidAppear()
// parentWindowController = self.view.window!.windowController
self.view.window!.delegate = self.view.window!.windowController as! S1W2WC
print(#function, "windowController", self.view.window!, self.view.window!.windowController)
}
by commenting out parentWindowController, notification don't go anymore to the WindowController…
Edited: I declared in ViewController:
var parentWindowController: NSWindowController? // Helps keep a reference to the controller
The proposed solutions are, in my opinion, hacks that can cause serious problems with memory management by creating circular references. You definitely can make instances of NSWindowController work as the window’s delegate. The proper way is to wire it up correctly in either code or in Interface Builder in Xcode. An example of how to do it properly is offered here.
If the delegate methods are not called is because the wiring up is not done correctly.
Another thing that must be done in Swift is when you add the name of the NSWindowController subclass in Interface Builder in Xcode is to check the checkbox of Inherits from Module. If you fail to do this, none of your subclass methods will be called.

makeFirstResponder does not always fire

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.

Subclassing NSArrayController causes objects from ManagedObjectContext doesn't show in NSTableView

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()

When to unregister KVO observation of Operation isFinished

In this simple code (Xcode 8.3), I create an Operation subclass instance, register for KVO observation of its isFinished property, and launch the operation by adding it to my queue:
class MyOperation : Operation {
override func main() {
print("starting")
print("finishing")
}
}
class ViewController: UIViewController {
let q = OperationQueue()
override func viewDidLoad() {
super.viewDidLoad()
let op = MyOperation()
op.addObserver(self, forKeyPath: #keyPath(MyOperation.isFinished), options: [], context: nil)
self.q.addOperation(op)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
print("Observed \(keyPath)")
if let op = object as? Operation {
op.removeObserver(self, forKeyPath: #keyPath(MyOperation.isFinished))
}
}
}
As you can see, I have an implementation of observeValue(forKeyPath..., of course, and my plan was to call removeObserver(forKeyPath... there.
The problem is that my app crashes with "MyOperation was deallocated while key value observers were still registered with it". We print "starting" and "finishing" but we never print "Observed"; the operation goes out of existence before I get my KVO notification.
This seems like a catch-22. If I can't remove the observer by observing isFinished, when am I supposed to do it? [I can work around this issue by adding to MyOperation my own KVO-observable property that I set at the end of main. But the notion that I should have to do this is very odd; isn't this exactly why isFinished is observable, so that I can do what I'm trying to here?]
After testing the exact same given code snippet on Xcode 8.2, it worked as it should, the console shows:
starting
finishing
Observed Optional("isFinished")
It seems that the reason of the issue is testing it on Xcode 8.3, probably it is a bug -or it might be a new behavior-. However, I would suggest to report it as a bug.
Apple changed #keyPath behavior in Swift 3.1 (source). Currently #keyPath(isFinished) returns "finished", it used to return "isFinished", and that was a bug. Here is the explanation as it can easily get confusing when using KVO and Operation class.
When you register an object for KVO notifications
textView.addObserver(self,
forKeyPath: #keyPath(UITextView.isEditable),
options: [.new, .old],
context: nil)
Foundation provides it (textView) with new setter implementation that calls willChangeValue(forKey:) and didChangeValue(forKey:) (this is done via isa-swizzling). The key that is passed to those methods is not getter (isEditable) not setter(setEditable:) but property name (editable).
#property(nonatomic,getter=isEditable) BOOL editable
This is why it is expected to receive editable from #keyPath(UITextView.isEditable).
Although Operation class has properties defined ad follows
#property (readonly, getter=isExecuting) BOOL executing;
#property (readonly, getter=isFinished) BOOL finished;
It expects to observe notifications for isFinished and isExecuting keys.
Upon completion or cancellation of its task, your concurrent operation object must generate KVO notifications for both the isExecuting and isFinished key paths to mark the final change of state for your operation
This forces us to use literal strings when posting those notifications.
IMHO this is a mistake made years ago which is really hard to recover from without breaking existing code.

NSNotificationCenter: Removing an Observer in Swift

I have a view controller with a button. When the button is pressed it adds an observer, like so:
func buttonPress(sender:UIButton){
NSNotificationCenter.defaultCenter().addObserverForName("buttonPressEvent", object:nil, queue:nil, usingBlock:{(notif) -> Void in
// code
})
}
When I dismiss this view controller, and then return to it and press the button the //code is executed twice. If I go away and come back again the //code is executed three times, and so on.
What I want to do is to remove the Observer before I add it again, so this code doesn't execute twice. Ive gone through the documentation here and Ive added this line of code just above where I add the Observer:
NSNotificationCenter.defaultCenter().removeObserver(self, name:"buttonPressEvent", object:nil)
But this isnt working.
Can anyone tell me where I'm going wrong?
When you use the 'blocks' based approach to observing notifications then self isn't in fact the observer. The function returns an object which acts as the observer:
func addObserverForName(_ name: String?,
object obj: AnyObject?,
queue queue: NSOperationQueue?,
usingBlock block: (NSNotification!) -> Void) -> NSObjectProtocol
You need to keep a reference to this returned object and pass it in as the observer when you call removeObserver
It's explained well in the Apple Doc here
Implemented it like this, seems to be working fine.
override func viewDidLoad()
{
super.viewDidLoad()
AddScreenShotNotification()
}
func AddScreenShotNotification() {
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(MyViewController.ScreenShotTaken),
name: UIApplicationUserDidTakeScreenshotNotification,
object: nil)
}
func ScreenShotTaken()
{
// do something
}
override func viewWillDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self)
}