NSView with a KVC property in Swift - xcode

I have a custom NSView class defined as:
class MyView: NSView
{
var someText: NSString
override func didChangeValueForKey(key: String)
{
println( key )
super.didChangeValueForKey( key )
}
// other stuff
}
What I want to be able to do is from outside of this class change the value of someText and have didChangeValueForKey notice that someText has changed so I can, for example, set needsDisplay to true for the view and do some other work.
How an I do this?

Are you sure you need KVC for this? KVC works fine in Swift, but there’s an easier way:
var SomeText: NSString {
didSet {
// do some work every time SomeText is set
}
}

There is no KVC mechanism for this because this isn't what KVC is for.
In Objective-C, you would implement the setter explicitly (or override if the property is originally from a superclass) and do your work there.
In Swift, the proper approach is the didSet mechanism.
didChangeValueForKey() is not part of KVC, it's part of KVO (Key-Value Observing). It is not intended to be overridden. It's intended to be called when one is implementing manual change notification (as a pair with willChangeValueForKey()).
More importantly, though, there's no reason to believe that it will be called at all for a property which is not being observed by anything. KVO swizzles the class in order to hook into the setters and other mutating accessors for those properties which are actually being observed. When such a property is changed (and supports automatic change notification), KVO calls willChangeValueForKey() and didChangeValueForKey() automatically. But for non-observed properties, those methods are not called.
Finally, in some cases, such as the indexed collection mutation accessors, KVO will use different change notification methods, such as willChange(_:valuesAtIndexes:forKey:) and didChange(_:valuesAtIndexes:forKey:).
If you really don't want to use didSet for some reason, you would use KVO to observe self for changes in the someText property and handle changes in observeValueForKeyPath(_:ofObject:change:context:). But this is a bad, clumsy, error-prone, inefficient way of doing a simple thing.

KVO and didSet are not mutually exclusive:
import Foundation
class C: NSObject {
dynamic var someText: String = "" {
didSet {
print("changed to \(someText)")
}
}
}
let c = C()
c.someText = "hi" // prints "changed to hi"
class Observer: NSObject {
init(_ c: C) {
super.init()
c.addObserver(self, forKeyPath: "someText", options: [], context: nil)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
print("observed change to \(object!.valueForKeyPath(keyPath!))")
}
}
let o = Observer(c)
c.someText = "test" // prints "changed to test" and "observed change to test"

I would add to Jaanus's answer that to make the property KVC compliant, you should declare it as dynamic var someText: NSString.
But if you don't need all the bells and whistles oh KVC, didSet is the way to go.
Update
As for didChangeValueForKey: – it is intended for the opposite, for you to notify value for key has changed (if it is not due to one of the cases covered by Foundation). You should use addObserver(_:forKeyPath:options:context:) and override observeValueForKeyPath(_:ofObject:change:context:) to be notified of changes.
Alternatively you can use one of many 3rd party solutions such as ReactiveCococa or Facebook's KVOController

Related

'#selector' refers to a method that is not exposed to Objective-C

The new Xcode 7.3 passing the parameter via addTarget usually works for me but in this case it's throwing the error in the title. Any ideas? It throws another when I try to change it to #objc
Thank you!
cell.commentButton.addTarget(self, action: #selector(FeedViewController.didTapCommentButton(_:)), forControlEvents: UIControlEvents.TouchUpInside)
The selector it's calling
func didTapCommentButton(post: Post) {
}
In my case the function of the selector was private. Once I removed the private the error was gone. Same goes for fileprivate.
In Swift 4
You will need to add #objc to the function declaration. Until swift 4 this was implicitly inferred.
You need to use the #objc attribute on didTapCommentButton(_:) to use it with #selector.
You say you did that but you got another error. My guess is that the new error is that Post is not a type that is compatible with Objective-C. You can only expose a method to Objective-C if all of its argument types, and its return type, are compatible with Objective-C.
You could fix that by making Post a subclass of NSObject, but that's not going to matter, because the argument to didTapCommentButton(_:) will not be a Post anyway. The argument to an action function is the sender of the action, and that sender will be commentButton, which is presumably a UIButton. You should declare didTapCommentButton like this:
#objc func didTapCommentButton(sender: UIButton) {
// ...
}
You'll then face the problem of getting the Post corresponding to the tapped button. There are multiple ways to get it. Here's one.
I gather (since your code says cell.commentButton) that you're setting up a table view (or a collection view). And since your cell has a non-standard property named commentButton, I assume it's a custom UITableViewCell subclass. So let's assume your cell is a PostCell declared like this:
class PostCell: UITableViewCell {
#IBOutlet var commentButton: UIButton?
var post: Post?
// other stuff...
}
Then you can walk up the view hierarchy from the button to find the PostCell, and get the post from it:
#objc func didTapCommentButton(sender: UIButton) {
var ancestor = sender.superview
while ancestor != nil && !(ancestor! is PostCell) {
ancestor = view.superview
}
guard let cell = ancestor as? PostCell,
post = cell.post
else { return }
// Do something with post here
}
Try having the selector point to a wrapper function, which in turn calls your delegate function. That worked for me.
cell.commentButton.addTarget(self, action: #selector(wrapperForDidTapCommentButton(_:)), forControlEvents: UIControlEvents.TouchUpInside)
-
func wrapperForDidTapCommentButton(post: Post) {
FeedViewController.didTapCommentButton(post)
}
As you know selector[About] says that Objective-C runtime[About] should be used. Declarations that are marked as private or fileprivate are not exposed to the Objective-C runtime by default. That is why you have two variants:
Mark your private or fileprivate method declaration by #objc[About]
Use internal, public, open method access modifier[About]

Functionality put in a convenience init - unusable in sub-classes?

Isn't functionality put in a convenience init - unusable in sub-classes?
If so, why are the Cocoa's interfaces for Swift defining so many initializers as convenience.
For example - I have a sub-class of NSWindowController and I would like to create a designated init, which will not get any parameters and should directly know what NIB file to instantiate with.
But I don't have any access to super.init's/methods to get the already implemented behaviour and build up on it. Here is the definition of the inits of NSWindowController:
class NSWindowController : NSResponder, NSCoding, NSSeguePerforming, NSObjectProtocol {
init(window: NSWindow?)
init?(coder: NSCoder)
convenience init(windowNibName: String)
convenience init(windowNibName: String, owner: AnyObject)
convenience init(windowNibPath: String, owner: AnyObject)
// ...
}
Instead I am forced to reimplement the NIB loading, thus duplicating and potentially getting it wrong.
Edit:
Here is a small passage from a blogpost by Mike Ash, mentioning NSWindowController subclasses and the reasoning behind what I do in my case is exactly the same:
NSWindowController provides a initWithWindowNibName: method. However, your subclass is built to work with only a single nib, so it's pointless to make clients specify that nib name. Instead, we'll provide a plain init method that does the right thing internally. Simply override it to call super and provide the nib name:
- (id)init
{
return [super initWithWindowNibName: #"MAImportantThingWindow"];
}
So it's possible in ObjectiveC, but how can this be done in Swift?
Convenience initializers are inherited in subclasses. They can be overriden, too.
In order to call init(windowNibName: String), you need to declare a convenience initializer to call it from, and you should call it on self, rather than super:
class MAImportantThingWindowController : NSWindowController {
override convenience init() {
self.init(windowNibName: "MAImportantThingWindow")
}
}

Swift weak delegate runtime error (bad access). Bug?

I have a problem with delegates in Swift (OSX). I have a view, connected to a delegate through a weak reference. Simplified code could be like this:
protocol MyProtocol: class {
func protocolFunc() -> Int
}
class MyController : MyProtocol {
func protocolFunc() -> Int { return 2 }
}
class MyView : NSView {
weak var delegate: MyProtocol?
func grabData {
var data = delegate?.protocolFunc()
}
}
When delegate?.protocolFunc() is called, the app crashes saying "bad access". It's like if the MyController instance had disappeared... But it has not. The MyController instance lives in a NSDocument subclass; and view's delegate is properly set.
The crash goes away if I declare the delegate to be strong. But the thing is I want the delegate to be weak. What's going on? To my eyes, the weak reference should work.
At the time of writing (Xcode 6 Beta 5), there's a bug with weak delegates. For the time being, all you can do until it is fixed is to change protocol MyProtocol: class to #objc protocol MyProtocol and avoid using any pure Swift classes in your protocol.
A temporary alternate solution would be to change this:
weak var delegate: MyProtocol?
to this:
weak var delegate: MyController?
Of course it defeats the purpose of MyProtocol, however, it allows you to use pure Swift classes while we wait for a proper fix for this.

Swift + Xcode 6 beta 3 + Core Data = awakeFromInsert not called?

Need help.
I'm creating new Document-based Core Data Cocoa project.
Add entity named 'Entity' into the core data model. Add 'creationDate' propery into it and set its type as Date. And create NSManagedObject subclass from 'Editor' menu.
Now I add into 'Entity.swift' file this code:
override func awakeFromInsert() {
super.awakeFromInsert()
self.creationDate = NSDate()
println("awakeFromInsert called")
}
Now in my NSPersistentDocument subclass I write such a init() method:
init() {
super.init()
var context = self.managedObjectContext
context.undoManager.disableUndoRegistration()
var entity = NSEntityDescription.insertNewObjectForEntityForName("Entity", inManagedObjectContext: context)
context.processPendingChanges()
context.undoManager.enableUndoRegistration()
println("\(entity)")
}
Everything compiles... BUT awakeFromInsert is never called! The interesting part is that 'entity' object ain't nil! It was created, but not initialized. And if I write this line in init method
entity.creationDate = NSDate()
then creationDate property will be set to a current date as expected.
But that's not all. If I debug execution step-by-step I can see that execution enters 'Entity.swift' file, but starts from the top of the file, then immediately drops and returns back to the NSPersistentDocument subclass file.
Tell me, is it a bug? Because I'm tired to fight with this nonsense. Thanks.
Accidentally I got it work: you have to add #objc(YourSubclass) before subclass declaration. I usually did #objc class MySubclass and turned out it does not work (don't know why).
WORKING:
#objc(YourSubclass)
class YourSubclass : NSManagedObject {
...
NOT WORKING:
#objc class YourSubclass : NSManagedObject {
...

An NSArrayController changes its selection : what is the best way to catch this event?

One can put an observer on the selectedIndex method of NSArrayController. This method has some drawbacks I think :
what will happen when the arrangedObjects is rearranged ? I admit this is not a very important problem
if we ask the observer to remember the old value of selectedIndex, it doesn't work. It is known but I cannot find again the link.
Why doesn't NSArrayController have a delegate ?
Is there another way to achieve what I want to do : launching some methods when the selection changes ?
Observe selection key of the NSArrayController (it is inherited from NSObjectController).
It will return either NSMultipleValuesMarker (when many objects are selected), NSNoSelectionMarker (when nothing is selected), or a proxy representing the selected object which can then be queried for the original object value through self key.
It will not change if rearranging objects did not actually change the selection.
You can also observe selectedObjects; in that case you won't need to deal with markers.
Providing hamstergene's excellent solution, in Swift 4.
In viewDidLoad, observe the key path.
arrayController.addObserver(self, forKeyPath: "selectedObjects", options: .new, context: nil)
In the view controller,
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let keyPath = keyPath else { return }
switch keyPath {
case "selectedObjects":
// arrayController.selectedObjects has changed
default:
break
}
}

Resources