如何使用 TextKit(不是 NSAttributedStrings)在 UITextView 中获取属性信息

Posted

技术标签:

【中文标题】如何使用 TextKit(不是 NSAttributedStrings)在 UITextView 中获取属性信息【英文标题】:how to get Attributed-Informations in UITextView with TextKit (not NSAttributedStrings) 【发布时间】:2016-04-02 22:13:31 【问题描述】:

我有一个带有 UITextKit 样式 (NSStrikethroughStyleAttributeName) 的 UITextView 和 TextKit:

这是我的代码:

@IBOutlet weak var textView: UITextView!
var dict = [String: AnyObject]()

override func viewDidLoad() 
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    let newFont = UIFont(name:"HelveticaNeue", size: textView.font!.pointSize)
    self.textView.font = newFont
    dict[NSFontAttributeName] = newFont
    
    let selectedRange: NSRange  = NSMakeRange(12,6)
    self.makeStrikeThrough(selectedRange)
    


func makeStrikeThrough(selectedRange: NSRange) 
    dict[NSStrikethroughStyleAttributeName] = 2
    self.textView.textStorage.beginEditing()
    self.textView.textStorage.setAttributes(dict, range: selectedRange)
    self.textView.textStorage.endEditing()

现在我必须有办法检测这个字体属性。有什么方法可以获取信息:

在 selectedRange: NSRange 12, 6 中,我使用属性 NSStrikethroughStyleAttributeName 和属性 2,也许作为 Array-Entry ???

欢迎提出任何想法!

【问题讨论】:

【参考方案1】:

唯一属性范围的离散化(w.r.t. 这些的左/右相邻范围)

您可以重复使用NSAttributedStringattributesAtIndex(location:effectiveRange:) 方法将整个属性字符串的范围编码为一个子范围列表,每个子范围都包含一组属性和值。

声明:

func attributesAtIndex(location: Int, 
     effectiveRange range: NSRangePointer) -> [String : AnyObject]

说明:

返回给定索引处字符的属性。

返回值:

index 处角色的属性。

更具体地说,使用attributesAtIndex(...) 创建一个对NSAttributedString 的扩展,它返回一个元组数组,元组定义为

第一个元组元素是一组不同属性的范围(或缺少属性)。 第二个元组元素本身就是一个元组数组,这些元组定义为: 第一个子元组元素是属性名称。 第二个子元组元素是属性值。 或者,第二个元组元素是一个[String: AnyObject] 数组,对应于属性及其各自的值(以下两种变体)

由多个属性赋予的属性字符串中的范围自然会返回一个包含多个元素的内部元组数组,而根本没有属性的范围将返回一个空的内部元组数组。

扩展名(s,两种选择)如下:

/* let 2nd tuple be an array of tuples itself */
extension NSAttributedString 
    func getAttributes() -> [(NSRange, [(String, AnyObject)])] 
        var attributesOverRanges : [(NSRange, [(String, AnyObject)])] = []
        var rng = NSRange()
        var idx = 0

        while idx < self.length 
            let foo = self.attributesAtIndex(idx, effectiveRange: &rng)
            var attributes : [(String, AnyObject)] = []

            for (k, v) in foo  attributes.append(k, v) 
            attributesOverRanges.append((rng, attributes))

            idx = max(idx + 1, rng.toRange()?.endIndex ?? 0)
        
        return attributesOverRanges
    


/* or, let 2nd tuple be a [String: AnyObject] dictionary */
extension NSAttributedString 
    func getAttributes() -> [(NSRange, [String: AnyObject])] 
        var attributesOverRanges : [(NSRange, [String: AnyObject])] = []
        var rng = NSRange()
        var idx = 0

        while idx < self.length 
            let foo = self.attributesAtIndex(idx, effectiveRange: &rng)
            attributesOverRanges.append((rng, foo))

            idx = max(idx + 1, rng.toRange()?.endIndex ?? 0)
        
        return attributesOverRanges
    

示例用法:

/* Example setup */
let fooString = "foo foo foo foo foo foo foo"
var fooAttrString = NSMutableAttributedString(string: fooString)
let selectedRange: NSRange = NSMakeRange(12,6)

// attr1: strikethrough over range (12,6) (12..<18)
var myRange = NSRange(location: 12, length: 6)
let strikeThroughAttr = [ NSStrikethroughStyleAttributeName: 2 ]
fooAttrString.addAttributes(strikeThroughAttr, range: myRange)

// attr2: font over range (16,8) (16..<24)
myRange = NSRange(location: 16, length: 8)
let fontAttr = [ NSFontAttributeName: UIFont(name:"HelveticaNeue", size: 20)! ]
fooAttrString.addAttributes(fontAttr, range: myRange)

/* Example usage: extension */
let attributesOverRanges = fooAttrString.getAttributes()
for (rng, attributes) in attributesOverRanges 
    print("Attributes over range \(rng):")
    attributes.forEach  print("\t\($0.0) = \($0.1)") 

/* Attributes over range (0,12):
   Attributes over range (12,4):
       NSStrikethrough = 2
   Attributes over range (16,2):
       NSFont = <UICTFont: 0x7fcfd860f0b0> font-family: "Helvetica Neue"; font-weight: normal; font-style: normal; font-size: 20.00pt
       NSStrikethrough = 2
   Attributes over range (18,6):
       NSFont = <UICTFont: 0x7fcfd860f0b0> font-family: "Helvetica Neue"; font-weight: normal; font-style: normal; font-size: 20.00pt
   Attributes over range (24,3):                       */

将上述内容应用于您的 UITextView 实例,特别是属性 textStorage

现在,UITextViewtextStorage 属性属于NSTextStorage 类型,它是NSMutableAttributedString 的(半具体)子类,它本身是NSAttributedString 的子类。因此,上面的扩展 getAttributes() 将可以访问并在 NSTextStorage 实例上同样有效,例如textView.textStorage 在您的问题中。

因此,使用与上述相同的扩展,我们设置了一个类似的示例,但针对具有属性 textStorage 属性的 UITextView

/* Example setup: UITextView:s 'textStorage' (type NSTextStorage) */
let fooString = "foo foo foo foo foo foo foo"

// attr1: strikethrough over range (12,6) (12..<18)
let strikeThroughRng = NSRange(location: 12, length: 6)
let strikeThroughAttr = [ NSStrikethroughStyleAttributeName: 2 ]

// attr2: font over range (16,8) (16..<24)
let fontRng = NSRange(location: 16, length: 8)
let fontAttr = [ NSFontAttributeName: UIFont(name:"HelveticaNeue", size: 20)! ]

// create text view and set attributes
let textView = UITextView()
textView.text = fooString
textView.textStorage.beginEditing()
textView.textStorage.addAttributes(strikeThroughAttr, range: strikeThroughRng)
textView.textStorage.addAttributes(fontAttr, range: fontRng)
textView.textStorage.endEditing()

示例用法、扩展:

/* Example usage: extension (uses first version above) */
let attributesOverRanges = textView.textStorage.getAttributes()
for (rng, attributes) in attributesOverRanges 
    print("Attributes over range \(rng):")
    attributes.forEach  print("\t\($0.0) = \($0.1)") 

/* Attributes over range (0,12):
       NSOriginalFont = <UICTFont: 0x7ff610d88e20> font-family: "Helvetica"; font-weight: normal; font-style: normal; font-size: 12.00pt
       NSFont = <UICTFont: 0x7ff610d88e20> font-family: "Helvetica"; font-weight: normal; font-style: normal; font-size: 12.00pt
   Attributes over range (12,4):
       NSOriginalFont = <UICTFont: 0x7ff610d88e20> font-family: "Helvetica"; font-weight: normal; font-style: normal; font-size: 12.00pt
       NSFont = <UICTFont: 0x7ff610d88e20> font-family: "Helvetica"; font-weight: normal; font-style: normal; font-size: 12.00pt
       NSStrikethrough = 2
   Attributes over range (16,2):
       NSFont = <UICTFont: 0x7ff610d80820> font-family: "Helvetica Neue"; font-weight: normal; font-style: normal; font-size: 20.00pt
       NSStrikethrough = 2
   Attributes over range (18,6):
       NSFont = <UICTFont: 0x7ff610d80820> font-family: "Helvetica Neue"; font-weight: normal; font-style: normal; font-size: 20.00pt
   Attributes over range (24,3):
       NSOriginalFont = <UICTFont: 0x7ff610d88e20> font-family: "Helvetica"; font-weight: normal; font-style: normal; font-size: 12.00pt
       NSFont = <UICTFont: 0x7ff610d88e20> font-family: "Helvetica"; font-weight: normal; font-style: normal; font-size: 12.00pt */

正如预期的那样,我们看到的结果与上面的NSAttributedString 示例相同,不同之处在于textView.textStorage 包含一些默认属性(NSFontNSOriginalFont)。


在属性字符串中搜索给定属性和属性值的首次出现

如果您愿意,您还可以编写一个扩展来搜索属性字符串以查找特定属性和值,使用 NSAttributedStringattribute(attrName:atIndex:effectiveRange:) 方法

声明:

func attribute(attrName: String, atIndex location: Int, 
               effectiveRange range: NSRangePointer) -> AnyObject?

说明:

返回具有给定角色名称的属性的值 在给定索引处,并通过引用属性的范围 适用。

返回值:

字符的attributeName属性值 index,或者nil,如果没有这样的属性。

更具体地说,创建一个扩展

在给定属性(例如NSFontAttributeName)的属性字符串中搜索给定值(例如2),并返回给定属性中此类属性部分第一次出现的范围(NSRange)字符串,或nil,如果找不到。

NSAttributedString扩展如下

/* find the range of (the first occurence of) a given 
   attribute 'attrName' for a given value 'forValue'. */
extension NSAttributedString 

    func findRangeOfAttribute(attrName: String, forValue value: AnyObject) -> NSRange? 

        var rng = NSRange()

        /* Is attribute (with given value) in range 0...X ? */
        if let val = self.attribute(attrName, atIndex: 0, effectiveRange: &rng) where val.isEqual(value)  return rng 

        /* If not, is attribute (with given value) anywhere in range X+1..<end? */
        else if
            let from = rng.toRange()?.endIndex where from < self.length - 1,
            let val = self.attribute(attrName, atIndex: from, effectiveRange: &rng) where val.isEqual(value)  return rng 

        /* if none of the above, return nil */
        return nil
    

示例用法:

/* Example */
let fooString = "foo foo foo foo foo foo foo"
var fooAttrString = NSMutableAttributedString(string: fooString)
let selectedRange: NSRange  = NSMakeRange(12,6)

let myRange = NSRange(location: 12, length: 6)
let attr = [ NSStrikethroughStyleAttributeName: 2 ]
fooAttrString.addAttributes(attr, range: myRange)

/* Example usage: extension */
if let rngOfFirstStrikethrough = fooAttrString.findRangeOfAttribute(NSStrikethroughStyleAttributeName, forValue: 2) 
    print(rngOfStrikethrough) // (12,6)

【讨论】:

超级,你非常全面的答案。我认为 func getAttributes() 的第一个案例是我正在搜索的 :-) 很多 THX! @UlliHeinelt 很高兴为您提供帮助。 直到现在我使用了 UITextKit 的特性(就像在我的示例中一样)和 not NSAttributedString (我的错误:-( )。如果有的话会很好来自 dfri 的具有 UITextKit 功能的 func getAttributes() 版本 @UlliHeinelt UITextFieldtextStorage 属性属于NSTextStorage 类型,它是NSAttributedString 的子类,因此上面对后者的扩展将可以访问并作为也适用于前者。我在上面的答案中间包含了这样一个例子。 不知道,NSTextStorage 是 NSAttributedString 的子类。现在我看到了 textStorage-sample。 :-) 这么多东西 ;-)

以上是关于如何使用 TextKit(不是 NSAttributedStrings)在 UITextView 中获取属性信息的主要内容,如果未能解决你的问题,请参考以下文章

NSTextAttachment 使用 textkit 时覆盖文本

初识 TextKit

再次将 textKit 从 Objective-C 转换为 Swift

如何使用 Text Kit 查找单行文本的高度

iOS7 TextKit:项目符号对齐

如何在Swift中运用Text Kit框架