Quick Summary
If you add a Swift property to an NSTextFieldCell subclass and you may suddenly start getting random crashes! Implement an override of [NSCell copyWithZone:] that retains any properties to fix this:
// Swift 3.0 | |
class TableViewTextFieldCell: NSTextFieldCell { | |
private var previousTextColor: NSColor? | |
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 | |
} | |
} |
Gory Details
I hit this in a little app that I’m working on, and I was a bit stumped as to what was happening. Using Zombies in Instruments revealed an overrelease of a color in a simple NSTextFieldCell subclass that added an NSColor property.
// Custom text colors don't automagically invert. | |
class TableViewTextFieldCell: NSTextFieldCell { | |
private var previousTextColor: NSColor? | |
override var backgroundStyle: NSView.BackgroundStyle { | |
get { | |
return super.backgroundStyle | |
} | |
set(newBackgroundStyle) { | |
// If we are going to light because we are selected, save off the old color so we can restore it | |
if self.backgroundStyle == .light && newBackgroundStyle == .dark { | |
previousTextColor = self.textColor | |
self.textColor = NSColor.white // or a named color? | |
} else if self.backgroundStyle == .dark && newBackgroundStyle == .light { | |
if previousTextColor != nil { | |
self.textColor = previousTextColor | |
previousTextColor = nil | |
} | |
} | |
super.backgroundStyle = newBackgroundStyle | |
} | |
} | |
} |
I’m using a View-Based NSTableView, and I know that a cell-based NSTableView would have some issues unless you implement [NSCell copyWithZone:], but that shouldn’t be needed for my particular case.
The Zombies seemed to reveal that the deallocated cell was doing something bad. I did a little bit of digging by the logging of addresses, and discovered my NSColor property was duplicated in a few locations. That was strange, as the instances should have been unique. I found out that it was being copied by AppKit! Here’s the backtrace I hit upon:
It looks like using a baseline constraint with Autolayout will cause it to copy the cell to determine the baseline. So, if you are using AutoLayout, be aware of implicit copies that might happen behind your back!
The trouble with [NSCell copyWithZone:] is that it uses NSCopyObject, which blindly assigns ivars from one instance to another and doesn’t do any proper memory management. I didn’t think this would still be an issue in Swift, but apparently it is! See my solution at the top where I simply retain the value during the copy.
I did some searching and numerous people are hitting this problem:
Developer Forums: Swift NSCopying on the Developer
StackOverflow: Copying NSTextFieldCell subclass in Swift causes crash
Swift.org: If NSCopyObject is called on a Swift class with stored properties, it can cause a crash