Adopting NSTextFinderBarContainer protocol in Swift forces variable initialization despite header comment - macos

I have an NSView subclass that implements the NSTextFinderBarContainer protocol. Part of the NSTextFinderBarContainer protocol is implementing
var findBarView: NSView { get set }
However the comment above this property in the original Objective-C header is:
This property is used by NSTextFinder to assign a find bar to a
container. The container may freely modify the view's width, but
should not modify its height. This property is managed by
NSTextFinder. You should not set this property.
Because Swift requires all instance variables to be initialized, how do I handle this situation? It appears Swift requires me to go against what Apple has wrote in the header: you should not set this property as it will be set/managed by the NSTextFinder itself.
If I don't override the NSView initializers I get:
Class 'ExampleContainerView' has no initializers
As expected since findBarView does not have an initial value.
The relevant parts of my Swift code are:
class ExampleContainerView: NSView, NSTextFinderBarContainer {
var findBarView : NSView
...
}
If I override the designated initializer to initialize findBarView as follows (ignoring Apple's comment in the header):
required init?(coder: NSCoder) {
findBarView = NSView(frame: NSRect())
super.init(coder: coder)
}
The app crashes after the NSTextFinder is sent the setFindBarContainer: message
-[NSView _setTextFinder:]: unrecognized selector sent to instance 0x6000001278a0
The object at 0x6000001278a0 is the NSView instance set in the overridden initializer above.

This appears fixed as of Xcode 7.0 beta 6. NSTextFinderBarContainer now declares findBarView as an optional NSView:
public var findBarView: NSView? { get set }
In addition, contentView() also changed to return an optional NSView:
optional public func contentView() -> NSView?
Making the property optional means there is no longer the contradiction of having the API comments say not to set findBarView, while having Swift require that all non-optional properties are initialized in in your initializers.

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]

How to store a non-standard persistent attribute in Core Data (ex. NSRect)?

I'm struggling with my first Core Data app and not having the smoothest ride :o(
I have a drawing app with a baseclass called DrawingObject which subclasses NSManagedObject and has two properties:
#NSManaged var frameAsValue: NSValue
var frame: NSRect
DrawingObject has a subclass DrawingRectangle. All have corresponding entities with fully qualified classnames set. The frameAsValue attribute is marked as transformable and frame is marked as Undefined transient. The problem is that I get an unrecognised selector error for the frameAsValue property when creating a DrawingRectangle.
I've seen suggestions to transform NSRect to a string to save it to Core Data but this seems error prone (localization) and hackish (if thats a proper word ;o). Here is the code for DrawingObject:
class DrawingObject: NSManagedObject {
#NSManaged var frameAsValue: NSValue
var frame: NSRect = NSZeroRect {
didSet {
frameAsValue = NSValue(rect: frame)
}
}
override func awakeFromInsert() {
frame = NSMakeRect(0, 0, 0, 0)
}
override func awakeFromFetch() {
frame = frameAsValue.rectValue
}
}
I'm now assuming that you have to declare all of the inherited properties in the class hierarchy in each entity. I don't have time to test this now, but will be back soon.
You have to set parent entity of DrawingRectangle to DrawingObject to be able to inherit the properties from the parent entity (just like in the class-hierarchy). You also have to set parent--child entity relationships if you intend to save different subclasses of a parent-entity/parent-class into the same to-many relationship.
For examble if you want to save different subclasses of DrawingObjects in a #NSManaged var objects: NSSet in a Drawing for example, you have to set the parent relationships on the entities to correspond to your NSManagedObject class-hierarchy. So in this case you would set the parent of the DrawingRectangle entity to DrawingObject.
Otherwise NSRect is saved as in the code shown in the question by setting the frame-property of the DrawingObject-entity to transient and optional and the frameAsValue as transformable. You should not specify a valuetransformer so Core Data will use the default value transformer which works perfectly in this case.

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")
}
}

Custom NSValueTransformer in xcode 6 with swift

Did anyone successfully implement a custom NSValueTransformer in xcode 6 beta with swift?
I have the following swift class:
import Foundation
class myTransformer: NSValueTransformer {
let amount = 100
override class func transformedValueClass() -> AnyClass!
{
return NSNumber.self
}
override func transformedValue(value: AnyObject!) -> AnyObject! {
return value.integerValue + amount
}
}
So all this transformer should do is, adding 100 to a given value in the gui.
As you can see, the transformer class appears now in the Value Transformer drop down in IB.
But if I choose this transformer the application crashes with:
2014-08-27 20:12:17.686 cdTest[44134:303]
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'Cannot find value transformer with name newTransformer'
Is it right to register this transformer in the AppDelegate with
override class func initialize() {
let newTransformer = myTransformer()
}
Does anyone know how this whole stuff should work?
kind regards!
martin
From Xcode release notes:
If you set a Swift subclass of NSValueTransformer as a binding’s value
transformer, the XIB or storyboard will contain an invalid reference
to the class, and the binding will not work properly at runtime. You
can either enter a mangled class name into the Value Transformer field
or add the #objc(…) attribute to the NSValueTransformer subclass to
solve this problem. (17495784)
From Swift guide:
To make your Swift class accessible and usable back in Objective-C,
make it a descendant of an Objective-C class or mark it with the #objc
attribute. To specify a particular name for the class to use in
Objective-C, mark it with #objc(<#name#>), where <#name#> is the name
that your Objective-C code will use to reference the Swift class. For
more information on #objc, see Swift Type Compatibility.
Solution:
Declare your class as #objc(myTransformer) class myTransformer: NSValueTransformer and then you can use "myTransformer" as name...
After you initialise newTransformer you should also include the line:
NSValueTransformer.setValueTransformer(newTransformer, forName: "myTransformer")
Then in your Interface Builder you should use myTransformer instead of newTransformer under the Value Transformer dropdown.

How to initialize a NSWindowController in Swift?

I want to initialize a window controller object from a nib file, quite easy right? But I simply can't get it to work.
According to my previous experience in ObjC, I've written down the following code:
init() {
super.init(windowNibName: "SplitWindowController")
}
And in the app delegate file, I simply init and displays the window:
var myWindowController: MyWindowController = MyWindowController()
myWindowController.showWindow(self)
myWindowController.window.makeKeyAndOrderFront(nil)
But the compiler gives me this error: Must call a designated initializer of the superclass 'NSWindowController'. And according to the Swift version of NSWindowController definition, there are only 3 designated initializers, namely init(), init(window), init(coder). I don't know what to do next. Shall I build a NSCoder from a nib file, which I don't know how to do?
You were almost there. You can indeed override init() as a convenience initialiser in a manner that is equivalent to the Obj-C code you got used to:
import Cocoa
class MyWindowController: NSWindowController {
override convenience init() {
self.init(windowNibName: "<xib name>")
}
}
Note that you are calling init(windowNibName:) on self, because init() being a convenience initialiser, you still inherit all the initialisers from the superclass. From documentation:
Rule 1: A designated initializer must call a designated initializer
from its immediate superclass.
Rule 2: A convenience initializer must call another initializer from
the same class.
Rule 3: A convenience initializer must ultimately call a designated
initializer.
Also, as #weichsel mentioned above, make sure you set the class of the File's Owner to your subclass of NSWindowController (in the example above, that would be MyWindowController) and then connect its window outlet with the window itself.
That being said, I'm not sure why is compiler asking for the override keyword to be added. Though NSWindowController is a subclass of NSResponder, which defines an init(), the following code compiles without issue even though it implements an equivalent inheritance hierarchy:
class A {
init() { }
}
class B: A {
init(Int) {
super.init()
}
convenience init(String) {
self.init(5)
}
}
class C: B {
convenience init() {
self.init("5")
}
}
NSWindowController has 2 designated initializers:
init(window: NSWindow!)
init(coder: NSCoder!)
When creating a subclass, you should invoke the designated initializer of its superclass. Recent versions of Xcode enforce this. Either via built-in language mechanism (Swift) or via NS_DESIGNATED_INITIALIZER macro (Objective-C).
Swift additionally requires that you call the superclasses designated initializer when you override a convenience initializer.
From the "Initialization: Designated Initializers and Convenience Initializers" section of Swift Programming Guide:
If the initializer you are overriding is a convenience initializer,
your override must call another designated initializer from its own
subclass, as per the rules described above in Initializer Chaining.
In your case, you should probably override init(window: NSWindow!) and call super's counterpart from there.

Resources