Search code examples
swiftcocoanstextfieldcell

Copying NSTextFieldCell subclass in Swift causes crash


I have been trying to create a subclass of NSTextFieldCell to use with a custom NSTextField (with Swift). However, my code breaks when trying to copy the subclassed cell. The basic code I have is

class XYTextFieldCell: NSTextFieldCell {
    var borderColor = NSColor.init(red: 0.5, green: 0.5, blue: 0.5, alpha: 1)        
    override init(imageCell image: NSImage?) {
        super.init(imageCell: image)
    }

    override init(textCell aString: String) {
        super.init(textCell: aString)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    deinit {
        Swift.print("Deinit XYTextFieldCell: \(unsafeAddressOf(self))")
    }
}

In AppDelegate (to try and simulate the crash in a small app), I have

func applicationDidFinishLaunching(aNotification: NSNotification) {
    let textFieldCell = XYTextFieldCell.init(textCell: "Test")
    Swift.print("TextFieldCell: \(unsafeAddressOf(textFieldCell))")
    print("textFieldCell.color: \(unsafeAddressOf(textFieldCell.borderColor))")
    copyTextFieldCell(textFieldCell)
}

func copyTextFieldCell(textFieldCell: XYTextFieldCell) {
    Swift.print("TextFieldCell (param): \(unsafeAddressOf(textFieldCell))")
    let copy = textFieldCell.copy() as! XYTextFieldCell
    Swift.print("TextFieldCell (copy): \(unsafeAddressOf(copy))")
    print("copy.color: \(unsafeAddressOf(copy.borderColor))")
}

The app crashes with

[NSColorSpaceColor release]: message sent to deallocated instance 0x600000075240

Full output is

TextFieldCell: 0x00006080000a61e0 
textFieldCell.color: 0x0000608000074840 
TextFieldCell (param): 0x00006080000a61e0 
TextFieldCell (copy): 0x00006080000a62a0 
copy.color: 0x0000608000074840 
Deinit XYTextFieldCell: 0x00006080000a62a0 
Deinit XYTextFieldCell: 0x00006080000a61e0 
2015-10-09 16:52:35.043 Test[86949:4746488] *** -[NSColorSpaceColor release]: message sent to deallocated instance 0x608000074840

It looks like the borderColor is not being retained correctly after the copy (and is being double released). I then tried to add a copy overload to try and force a copy of the borderColor.

override func copyWithZone(zone: NSZone) -> AnyObject {
        let myCopy = super.copyWithZone(zone) as! XYTextFieldCell
        myCopy.borderColor = borderColor.copyWithZone(zone) as! NSColor

        return myCopy
    }

But, it still crashes with the same error

TextFieldCell: 0x00006080000ab4c0 textFieldCell.color: 0x00006080000769c0    
TextFieldCell (param): 0x00006080000ab4c0 
TextFieldCell (copy): 0x00006080000ab520 
copy.color: 0x00006080000769c0 
Deinit XYTextFieldCell: 0x00006080000ab520 
Deinit XYTextFieldCell: 0x00006080000ab4c0 
2015-10-09 16:54:54.248 Test[87031:4749016] *** -[NSColorSpaceColor release]: message sent to deallocated instance 0x6080000769c0

I can avoid the crash by doing a init of a new XYTextFieldCell inside the copyWithZone: (instead of calling super.copyWithZone). But, this would mean that I have to manually reassign all the superclass defined properties to my copy as well.

Is there a way to do the copy of the NSTextFieldCell correctly, so that it doesn't double release my subclass properties. I am noticing this behavior when subclassing from NSButtonCell as well. But, if I don't inherit from either (XYTextFieldCell is a root Swift class), then it works fine. Thanks


Solution

  • It looks like I have been caught by NSCopyObject() as detailed here. The gist of the link is that NSCell subclasses uses NSCopyObject() to conform to the NSCopying protocol - and NSCopyObject() doesn't seem to set retain counts correctly.

    The solution outlined there is in Objective-C, and I am not sure how to translate it to Swift (as it involves directly accessing Objective-C ivars in the copyWithZone: method). So, I've had to implement the basic subclass in objective-c (and do the copy in objective-c). I then bridge that to a swift subclass

    RMTextFieldCell.h (in objective-c)

    #import <Cocoa/Cocoa.h>
    
    @interface RMTextFieldCell : NSTextFieldCell
    
    @property (nonatomic, strong) NSColor* borderColor;
    
    @end
    

    RMTextFieldCell.m (in objective-c)

    #import "RMTextFieldCell.h"
    
    @implementation RMTextFieldCell
    
    -(id) initTextCell:(NSString *)aString
    {
        self = [super initTextCell:aString];
        [self setup];
    
        return self;
    }
    
    -(id) initImageCell:(NSImage *)image
    {
        self = [super initImageCell:image];
        [self setup];
    
        return self;
    }
    
    -(id) initWithCoder:(NSCoder *)aDecoder
    {
        self = [super initWithCoder:aDecoder];
        [self setup];
    
        return self;
    }
    
    -(RMTextFieldCell*) copyWithZone:(NSZone*) zone
    {
        RMTextFieldCell* copy = [super copyWithZone:zone];
        copy->_borderColor = nil;
        copy.borderColor = self.borderColor;
    
        return copy;
    }
    
    -(void) setup
    {
        self.borderColor = [NSColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
    }
    
    @end
    

    RMSwiftTextFieldCell.swift (in swift)

    import Cocoa
    
    class RMSwiftTextFieldCell: RMTextFieldCell {
    
    
        override init(imageCell image: NSImage?) {
            super.init(imageCell: image)
        }
    
        override init(textCell aString: String) {
            super.init(textCell: aString)
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
    
        deinit {
            Swift.print("Deinit RMTextFieldCell: \(unsafeAddressOf(self))")
        }
    }
    

    This is a tad convoluted, but, seems to work in my initial testing. If anybody has a better solution, I would appreciate it :) Thanks

    Edit: It looks like I don't even need to implement copyWithZone: in the objective-c subclass.