在 Swift 中复制 NSTextFieldCell 子类会导致崩溃

Copying NSTextFieldCell subclass in Swift causes crash

我一直在尝试创建 NSTextFieldCell 的子class 以与自定义 NSTextField(使用 Swift)一起使用。但是,我的代码在尝试复制 subclassed 单元格时中断。我的基本代码是

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

在 AppDelegate(尝试模拟小应用程序中的崩溃)中,我有

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

应用程序崩溃

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

完整输出为

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

看起来 borderColor 在复制后没有正确保留(并且被双重释放)。然后我尝试添加复制重载以尝试强制复制 borderColor。

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

        return myCopy
    }

但是,它仍然因同样的错误而崩溃

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

我可以通过在 copyWithZone: 中初始化一个新的 XYTextFieldCell 来避免崩溃:(而不是调用 super.copyWithZone)。但是,这意味着我必须手动将所有 superclass 定义的属性重新分配给我的副本。

有没有办法正确地复制 NSTextFieldCell,这样它就不会双重释放我的 subclass 属性。当从 NSButtonCell 进行 subclassing 时,我也注意到了这种行为。但是,如果我不继承任何一个(XYTextFieldCell 是一个根 Swift class),那么它工作正常。谢谢

看来我已经被 NSCopyObject() 捕获了,详情 here。 link 的要点是 NSCell 子类使用 NSCopyObject() 来符合 NSCopying 协议——而 NSCopyObject() 似乎没有正确设置保留计数。

那里概述的解决方案在 Objective-C 中,我不确定如何将其转换为 Swift(因为它涉及在 copyWithZone: 方法中直接访问 Objective-C ivars) .因此,我不得不在 objective-c 中实现基本子类(并在 objective-c 中进行复制)。然后我将其桥接到 swift 子类

RMTextFieldCell.h(在objective-c)

#import <Cocoa/Cocoa.h>

@interface RMTextFieldCell : NSTextFieldCell

@property (nonatomic, strong) NSColor* borderColor;

@end

RMTextFieldCell.m(在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(在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))")
    }
}

这有点令人费解,但在我的初始测试中似乎有效。如果有人有更好的解决方案,我将不胜感激:) 谢谢

编辑:看起来我什至不需要在 objective-c 子类中实现 copyWithZone:。

我知道这是一个老问题,但我想我会回答它。您需要为复制的对象添加保留。这是我为可选的 NSColor 做的:

class TableViewTextFieldCell: NSTextFieldCell {

    private var previousTextColor: NSColor?

    // Super newbie mistake of forgetting to implement copyWithZone; why would we need it if we aren't using a cell-based NSTableView?  This shouldn't be needed..but apparently NSTextFieldCell's baseline measurement with autolayout does a copy! who would have guessed. NSCell's implementation does a NSCopyObject, which doesn't retain ivars
    override func copy(with zone: NSZone? = nil) -> Any {
        let result: TableViewTextFieldCell = super .copy(with: zone) as! TableViewTextFieldCell
        if let previousTextColor = result.previousTextColor {
            // Add the needed retain now
            let _ = Unmanaged<NSColor>.passRetained(previousTextColor)
        }
        return result
    }
}