xcodeswift2nsattributedstringnsfont

how to get Attributed-Informations in UITextView with TextKit (not NSAttributedStrings)


I have a UITextView and TextKit with an UITextKit-Style (NSStrikethroughStyleAttributeName):

enter image description here

This is my code:

@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()
}

Now i must have a way to detect this Font-Attribute. Is there any way to get the Info:

In the selectedRange: NSRange 12, 6 i use the Attribute NSStrikethroughStyleAttributeName with the property 2, perhaps as an Array-Entry???

Any idea is welcome!


Solution

  • Discretization of the uniquely attributed ranges (w.r.t. left/right neighbouring ranges of these)

    You can make use of repeated use of the attributesAtIndex(location:effectiveRange:) method of NSAttributedString to encode the range over the full attributed string into a list of sub-ranges which each hold a set of attributes and values to these.

    Declaration:

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

    Description:

    Returns the attributes for the character at a given index.

    Return value:

    The attributes for the character at index.

    More specifically, use attributesAtIndex(...) to create an extension to NSAttributedString that returns and array of tuples, with tuples defined as

    Ranges in the attributed string which are attributed by several attributes will naturally return an inner tuple array of several elements, whereas ranges that are not attributed at all will return an empty inner tuple array.

    The extension(s, two alternatives) as follows:

    /* 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 usage:

    /* 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):                       */
    

    Applying the above to your UITextView instance, specifically property textStorage

    Now, the textStorage property of UITextView is of type NSTextStorage, which is a (semi concrete) subclass of NSMutableAttributedString, which itself is a subclass of NSAttributedString. Hence, the extension getAttributes() above will be accessible and work just as well on NSTextStorage instances, e.g. textView.textStorage in your question.

    Hence, using the same extensions as above, we set up a similar example but for an UITextView with an attributed textStorage property.

    /* 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:

    /* 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 */
    

    As expected, we see the same results as in the NSAttributedString example above, with the difference that the textView.textStorage contains some default attributes (NSFont, NSOriginalFont).


    Search attributed string for first occurrence of a given attribute and attribute value

    If you'd like you could also write an extension to search an attributed string for a specific attribute and value, making use of the attribute(attrName:atIndex:effectiveRange:) method of NSAttributedString

    Declaration:

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

    Description:

    Returns the value for an attribute with a given name of the character at a given index, and by reference the range over which the attribute applies.

    Return value:

    The value for the attribute named attributeName of the character at index, or nil if there is no such attribute.

    More specifically, creating an extension that

    NSAttributedString extension as follows

    /* 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 usage:

    /* 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)
    }