Episode #208

Syntax Highlighting with TextKit

17 minutes
Published on February 18, 2016

This video is only available to subscribers. Get access to this video and 584 others.

In this episode Sam Soffes joins us again to show how to implement some rudimentary syntax highlighting of text while you type using TextKit. This builds on the concepts we learned in episode 207, so start there first!

About the Author

Sam Soffes is an experienced iOS developer and regular contributor to NSScreencast, with extensive experience in TextKit. You find Sam on Twitter.

Episode Links

Custom Text Storage Class

We can leverage the processEditing method to do our work:

class SyntaxTextStorage: BaseTextStorage {
    override func processEditing() {
        let text = string as NSString

        setAttributes([
            NSFontAttributeName: UIFont.systemFontOfSize(18)
        ], range: NSRange(location: 0, length: length))

        text.enumerateSubstringsInRange(NSRange(location: 0, length: length), options: .ByWords) { [weak self] string, range, _, _ in
            guard let string = string else { return }
            if string.lowercaseString == "red" {
                self?.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: range)
            } else if string.lowercaseString == "bold" {
                self?.addAttribute(NSFontAttributeName, value: UIFont.boldSystemFontOfSize(18), range: range)
            }
        }

        super.processEditing()
    }
}

Note that this is extremely naive, as we're re-processing the entire string on every keystroke. It would be more efficient to see what changed and only recompute those attributes, however that is fairly complicated.

The base class we're using is a simple one that just leverages NSAttributedString as a backing storage:

class BaseTextStorage: NSTextStorage {

    // MARK: - Properties

    private let storage = NSMutableAttributedString()


    // MARK: - NSTextStorage

    override var string: String {
        return storage.string
    }

    override func attributesAtIndex(location: Int, effectiveRange: NSRangePointer) -> [String : AnyObject] {
        return storage.attributesAtIndex(location, effectiveRange: effectiveRange)
    }

    override func replaceCharactersInRange(range: NSRange, withString string: String) {
        let beforeLength = length
        storage.replaceCharactersInRange(range, withString: string)
        edited(.EditedCharacters, range: range, changeInLength: length - beforeLength)

    }

    override func setAttributes(attributes: [String : AnyObject]?, range: NSRange) {
        storage.setAttributes(attributes, range: range)
        edited(.EditedAttributes, range: range, changeInLength: 0)
    }
}