The Problem
One of the controls included with the UIKit framework of iOS is the UITextView. It handles all text editing tasks including line wrapping, copy and paste operations, and undo. The only thing it lacks is text styling. But starting from iOS 4.0 Apple added the CoreText framework. This framework gives you the ability to draw text with different styles and colors. The problem is integrating the text styling features of CoreText with the text editing features of UIKit.
The only successful reports of doing syntax highlighting involve either rewriting all editing features of an UITextView like in OmniUI, or using a UIWebView with JavaScript to handle the highlighting as described here and here.
I found a way of reusing a UITextView with custom drawing using CoreText.
The Solution
Let us start by creating a view that draws an attributed string. An NSAttributedString is what CoreText uses to draw text that has styling information like colors. Here is our class definition:
#import <UIKit/UIKit.h> @interface AttributedTextView : UIView @property (strong, nonatomic) NSAttributedString* string; @endPretty simple. And this is the implementation of the drawing code:
- (void)drawRect:(CGRect)rect {
// flip the coordinate system
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
// Get the text bounds
CGSize size = self.bounds.size;
CGRect r = CGRectMake(MARGIN, MARGIN, size.width - 2*MARGIN, size.height - 2*MARGIN);
// Get line height
CTFontRef font = (CTFontRef)[string attribute:(id)kCTFontAttributeName atIndex:0 effectiveRange:NULL];
CGFloat lineHeight = [[self class] lineHeight:font];
// Draw lines
CGFloat y = r.size.height - MARGIN + 2;
NSCharacterSet* cs = [NSCharacterSet newlineCharacterSet];
NSRange range = NSMakeRange(0, string.length);
while (true) {
// Find next line break
NSRange next = [string.string rangeOfCharacterFromSet:cs options:NSLiteralSearch range:range];
if (next.location == NSNotFound)
break;
// Keep track of ranges
NSUInteger len = next.location - range.location;
NSRange lineRange = NSMakeRange(range.location, len);
range.location += len + 1;
range.length -= len + 1;
// Draw line
if (len > 0)
[self drawLine:lineRange offset:y context:context];
y -= lineHeight;
}
// Draw last line
if (range.length > 0)
[self drawLine:range offset:y context:context];
}
- (void)drawLine:(NSRange)range offset:(CGFloat)offset context:(CGContextRef)context {
// Get one line of text
NSAttributedString* s = [string attributedSubstringFromRange:range];
// Draw line
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)s);
CGContextSetTextPosition(context, MARGIN, offset);
CTLineDraw(line, context);
CFRelease(line);
}
An important method here is lineHeight: which computes the number of pixels to move down after each line. This has to match the line height of the UITextView exactly or else the selection markers and the caret will be off when editing. From trial an error I got this, but you may have to adjust for different fonts:
+ (CGFloat)lineHeight:(CTFontRef)font {
CGFloat ascent = CTFontGetAscent(font);
CGFloat descent = CTFontGetDescent(font);
CGFloat leading = CTFontGetLeading(font);
return ceilf(ascent + descent + leading);
}
Now for the actual text view. We have to inherit from UITextView, add our custom AttributedTextView as a subview and make sure the text view is refreshed every time the text changes:
- (void)setup {
// The internal delegate notifies the AttributedTextView of text changes
internalDelegate = [[HighlightingTextViewDelegate alloc] init];
self.delegate = internalDelegate;
// The syntax highlighter is in charge of converting an NSString to an NSAttributedString
syntaxHighlighter.font = self.font;
// Create the AttributedTextView and add as a subview, covering the default text
attributedTextView = [[AttributedTextView alloc] init];
attributedTextView.string = [syntaxHighlighter highlight:self.text];
[self addSubview:attributedTextView];
for (UIView* view in self.subviews) {
if ([view isKindOfClass:NSClassFromString(@"UIWebDocumentView")]) {
internalDocumentView = view;
break;
}
}
}
- (void)layoutSubviews {
[super layoutSubviews];
attributedTextView.bounds = internalDocumentView.bounds;
attributedTextView.center = internalDocumentView.center;
}
- (void)setNeedsDisplay {
[super setNeedsDisplay];
attributedTextView.string = [syntaxHighlighter highlight:self.text];
[attributedTextView setNeedsDisplay];
}
- (void)setText:(NSString *)text {
[super setText:text];
attributedTextView.string = [syntaxHighlighter highlight:self.text];
}
@interface HighlightingTextViewDelegate : NSObject@end @implementation HighlightingTextViewDelegate - (void)textViewDidChange:(UITextView *)textView { [textView setNeedsDisplay]; } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [scrollView setNeedsDisplay]; } @end
Edit: Make sure that the font used in the NSAttributedString returned by the syntax highlighter matches the font set in the TextView. Your highlight: method should have something like this:
- (NSAttributedString*)highlight:(NSString*)text {
if (text == nil)
return nil;
NSMutableAttributedString* string = [[NSMutableAttributedString alloc] initWithString:text];
NSUInteger length = [string length];
NSRange wholeRange = NSMakeRange(0, length);
// The font has to match what is set for the text vuew
CTFontRef ctFont = CTFontCreateWithName((__bridge CFStringRef)_font.familyName, _font.pointSize, NULL);
[string addAttribute:(id)kCTFontAttributeName
value:(__bridge id)ctFont
range:wholeRange];
CFRelease(ctFont);
// Set the default text color
[string addAttribute:(id)kCTForegroundColorAttributeName
value:(__bridge id)_defaultColor.CGColor
range:wholeRange];
// Disable ligatures if you are using a fixed-width font
[string addAttribute:(id)kCTLigatureAttributeName
value:[NSNumber numberWithInt:0]
range:wholeRange];
// Add more attributes to the text
}
Here is a simple iPad app with Lua highlighting in action:
You can download this project along with the rest of the code from GitHub.Conclusion
It turns out that an UITextView can be made to render attributed text, even though it is not the cleanest implementation. Also it remains to be seen if such modifications to an UITextView will pass the App Store scrutiny. If you submit an app that uses the code do let me know whether the app gets approved or rejected.
The complete sources can be found here. It is under the Apache License version 2.0, but I would appreciate getting credit if you release an app that uses it.

hey awesome :) thanks for your great post, i have been waiting for something like that for months...
ReplyDeletewould you also release the syntax-hightler.m file to help us understand how to implement other languages?
DeleteI just added a demo project with the Lua syntax highlighter to GitHub. Enjoy!
DeleteHi Alejandro Isaza,
ReplyDeleteThanks for your nice code. I have one problem, when I put more html code on the UITextview in your sample code, it behaves very strange when I'm typing, It doesn't type where the cursor is, it types like 4 lines over the cursor position. It happens when the text is not wrapped in the Uitextview. How can I fix this? I will appreciate your help.
Thanks
James
I have this same issue, when I try to add some code the cursor gets messed up. I tried to add new detections with a different color and range, detecting symbols like "=", "(", and ")". It would not work however, and symbols were not detected when I tried implementing them in the original code either.
DeleteI very much appreciate our tutorial, do you know how I could fix/go about this?
Thanks!
Did you try the demo project? Give that a try. I know there are still a couple of issues like line wrapping, I'll revisit the code and try to improve it as soon as I get a chance.
DeleteHi, i am , jhon bonachon, i have made, something like this but much more simply :P
ReplyDeletei would like if i can send you my code and all people see it, it work in a WebView and use javascript, i am not the real author because i get from others people that help me, however i did my code alone for my job, and would like to share with the community and help me to check my code.
thanks in advance.
Hi Alejandro,thanks for your sharing. I have an issue when I try the sample code. I typed a tab then the next character after tab was not in right position. How can I fix this case? Thanks before :)
ReplyDeleteRemuz
Remuz, The best would be replacing tab characters with spaces. You can do this in the textView:shouldChangeTextInRange:replacementText: delegate method of the TextView.
Delete