I have been unable to find anything that works on the subject of using an attributed text in a NSTextField
with a NumberFormatter
. What I want to accomplish is very simple. I would like to use a NumberFormatter
on an editable NSTextField
with attributed text and keep the text attributed.
Currently, I have subclassed NSTextFieldCell
and implemented it as so:
class AdjustTextFieldCell: NSTextFieldCell {
required init(coder: NSCoder) {
super.init(coder: coder)
let attributes = makeAttributes()
allowsEditingTextAttributes = true
attributedStringValue = AttributedString(string: stringValue, attributes: attributes)
//formatter = TwoDigitFormatter()
}
func makeAttributes() -> [String: AnyObject] {
let style = NSMutableParagraphStyle()
style.minimumLineHeight = 100
style.maximumLineHeight = 100
style.paragraphSpacingBefore = 0
style.paragraphSpacing = 0
style.alignment = .center
style.lineHeightMultiple = 1.0
style.lineBreakMode = .byTruncatingTail
let droidSansMono = NSFont(name: "DroidSansMono", size: 70)!
return [NSParagraphStyleAttributeName: style, NSFontAttributeName: droidSansMono, NSBaselineOffsetAttributeName: -60]
}
}
This implementation adjusts the text in the NSTextField
instance to have the shown attributes. When I uncomment the line that sets the formatter
property the NSTextField
loses its attributes. My NumberFormatter
is as follows:
class TwoDigitFormatter: NumberFormatter {
override init() {
super.init()
let customAttribs = makeAttributes()
textAttributesForNegativeValues = customAttribs.attribs
textAttributesForPositiveValues = customAttribs.attribs
textAttributesForZero = customAttribs.attribs
textAttributesForNil = customAttribs.attribs
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
let maxLength = 2
let wrongCharacterSet = CharacterSet(charactersIn: "0123456789").inverted
override func isPartialStringValid(_ partialString: String, newEditingString newString: AutoreleasingUnsafeMutablePointer<NSString?>?, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
if partialString.characters.count > maxLength {
return false
}
if partialString.rangeOfCharacter(from: wrongCharacterSet) != nil {
return false
}
return true
}
override func attributedString(for obj: AnyObject, withDefaultAttributes attrs: [String : AnyObject]? = [:]) -> AttributedString? {
let stringVal = string(for: obj)
guard let string = stringVal else { return nil }
let customAttribs = makeAttributes()
var attributes = attrs
attributes?[NSFontAttributeName] = customAttribs.font
attributes?[NSParagraphStyleAttributeName] = customAttribs.style
attributes?[NSBaselineOffsetAttributeName] = customAttribs.baselineOffset
return AttributedString(string: string, attributes: attributes)
}
func makeAttributes() -> (font: NSFont, style: NSMutableParagraphStyle, baselineOffset: CGFloat, attribs: [String: AnyObject]) {
let style = NSMutableParagraphStyle()
style.minimumLineHeight = 100
style.maximumLineHeight = 100
style.paragraphSpacingBefore = 0
style.paragraphSpacing = 0
style.alignment = .center
style.lineHeightMultiple = 1.0
style.lineBreakMode = .byTruncatingTail
let droidSansMono = NSFont(name: "DroidSansMono", size: 70)!
return (droidSansMono, style, -60, [NSParagraphStyleAttributeName: style, NSFontAttributeName: droidSansMono, NSBaselineOffsetAttributeName: -60])
}
}
As you can see from the code directly above I have tried:
textAttributesFor...
properties.attributedString(for obj: AnyObject, withDefaultAttributes attrs: [String : AnyObject]? = [:])
I have tried both these solution separately from each other and together, of which none of the attempts worked.
TLDR:
Is it possible to use attributed text and a NumberFormatter
at the same time? If so, how? If not, how can I limit a NSTextField
with attributed text to digits only and two characters without using a NumberFormatter
?
For anyone in the future who wants to achieve the behavior I was able to do it by subclassing NSTextView
and essentially faking a NSTextField
like so:
class FakeTextField: NSTextView {
required init?(coder: NSCoder) {
super.init(coder: coder)
delegate = self
let droidSansMono = NSFont(name: "DroidSansMono", size: 70)!
configure(lineHeight: frame.size.height, alignment: .center, font: droidSansMono)
}
func configure(lineHeight: CGFloat, alignment: NSTextAlignment, font: NSFont) {
//Other Configuration
//Define and set typing attributes
let style = NSMutableParagraphStyle()
style.minimumLineHeight = lineHeight
style.maximumLineHeight = lineHeight
style.alignment = alignment
style.lineHeightMultiple = 1.0
typingAttributes = [NSParagraphStyleAttributeName: style, NSFontAttributeName: font]
}
}
extension TextField: NSTextViewDelegate {
func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString: String?) -> Bool {
if let oldText = textView.string, let replacement = replacementString {
let newText = (oldText as NSString).replacingCharacters(in: affectedCharRange, with: replacement)
let numberOfChars = newText.characters.count
let wrongCharacterSet = CharacterSet(charactersIn: "0123456789").inverted
let containsWrongCharacters = newText.rangeOfCharacter(from: wrongCharacterSet) != nil
return numberOfChars <= 2 && !containsWrongCharacters
}
return false
}
}
With this class, all you need to do is set the NSTextView
in your storyboard to this class and change the attributes and shouldChangeTextIn
to suit your needs. In this implementation, the "TextField" has various attributes set and is limited to two characters and only digits.