如何将 NSNumberFormatter 与 AttributedString 一起使用?

How to use a NSNumberFormatter with an AttributedString?

我找不到任何关于在 NSTextFieldNumberFormatter 中使用属性文本的主题。我想要完成的事情非常简单。我想在带有属性文本的可编辑 NSTextField 上使用 NumberFormatter 并保留文本属性。

目前,我已经将 NSTextFieldCell 子类化并按如下方式实现:

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]
  }


}

此实现调整 NSTextField 实例中的文本以具有显示的属性。当我取消注释设置 formatter 属性 的行时,NSTextField 将失去其属性。我的NumberFormatter如下:

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

正如您从上面的代码中看到的那样,我已经尝试过:

  1. 正在设置 textAttributesFor... 属性。
  2. 覆盖attributedString(for obj: AnyObject, withDefaultAttributes attrs: [String : AnyObject]? = [:])

我已经分别尝试过这两种解决方案,也尝试过一起尝试,其中 none 次尝试成功了。

TLDR:
是否可以同时使用属性文本和 NumberFormatter?如果是这样,如何?如果不是,我如何在不使用 NumberFormatter 的情况下将带有属性文本的 NSTextField 限制为仅数字和两个字符?

对于未来想要实现我能够通过子classing NSTextView 并且本质上像这样伪造 NSTextField 来实现的行为的任何人:

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

有了这个 class,您需要做的就是将故事板中的 NSTextView 设置为这个 class 并更改属性和 shouldChangeTextIn 以满足您的需要.在此实现中,"TextField" 设置了各种属性,并且仅限于两个字符和数字。

您将 NSNumberFormatter 子类化为以下内容:

final class ChargiePercentageNumberFormatter: NumberFormatter {

    override func attributedString(for obj: Any, withDefaultAttributes attrs: [NSAttributedString.Key : Any]? = nil) -> NSAttributedString? {
        guard let obj = obj as? NSNumber else {
            return nil;
        }
        guard let numberString = self.string(from: obj) else {
            return nil
        }
        let font = NSFont(name: "Poppins-Regular", size: 16)!
        let attrs: [NSAttributedString.Key: Any] = [.font: font, .kern: 1.2]

        let result = NSAttributedString(string: numberString, attributes: attrs)
        return result
    }

}