views:

2667

answers:

4

Hi,

I am creating a syntax highlighter for the iPhone and in order to display text with multiple formats, I have sub-classed UIView and modified the drawRect: method so that each line is displayed with the proper syntax highlighting (highlighting is done earlier with RegEx, text is drawn with CGContextShowTextAtPoint() one line at a time). Everything works ok, I store each line of text as an NSString in an NSMutableArray, I handle the keyboard through a hidden UITextField and its delegate methods, the cursor is a blinking CALayer that can be moved around with touches and I have a custom scroll view that handles scrolling. However, I have two problems that I can't seem to wrap my head around:

  1. Word wrap, right now the text just keeps going off the left end of the screen. To keep things fast I only redraw the portions of the view that have changed (usually just the line being edited, but sometimes the lines below as well e.g. if you press return halfway through the document) with setNeedsDisplayInRect:. This makes word wrap complicated because then you have to draw more than one line on the screen, even though it still is only one object in the array.

  2. UIViews have a maximum content size of 1024x1024 which equates to about 64 lines. I need the ability to display more than that. I am thinking about using multiple CALayers one after another, but I am having trouble drawing content to the layers (using drawLayer:inContext: and drawInContext:).

So my questions are:

  • Does anyone have any, even general, suggestions about how to accomplish either of these two points. Or,
  • Has someone already written a custom text-drawing view that handles these things that I could use instead.

Thanks,

Kyle

EDIT: The scrolling problem is pretty much solved, however I am still having trouble with word-wrap. My trouble is that everything is done by line: the view updates one line at a time, the text is stored as an array of lines, the highlighter highlights one line at a time, etc. and having a single index in the array (one line of text) take up multiple lines on the screen raises some problems, for example, I had to implement my own movable cursor and when you move the cursor it needs to be able to turn a display line (found by dividing touch.x by the line height) into a text line (an index in the array). Any ideas?

A: 

It may be best to draw your text within a CATiledLayer hosted within your UIView, in order to get around the 1024x1024 texture size limit (which appears to actually be 2048x2048 on all existing devices). For an example of text drawing within a CALayer, I'd refer you to the CPTextLayer class within the Core Plot framework. This layer (which inherits from the CPLayer class within that same framework) does cross-platform (Mac and iPhone) text rendering in a CALayer. You might be able to extend it to work as a CATiledLayer for longer text blocks.

One thing to be aware of is that platform-specific drawAtPoint: methods are used in this layer, instead of the CGContextShowTextAtPoint() function you are using. The reason for this is that CGContextShowTextAtPoint() only works on ASCII text, meaning that you can't do Unicode text rendering with it. There is an example of using CGContextShowTextAtPoint() to draw text, within a #define'd out portion at the bottom of the renderAsVectorInContext: method.

Brad Larson
Thanks for your answer, do you know if drawAtPoint: is significantly slower than CGContextShowTextAtPoint()? I assume it is, because Obj-C functions are generally slower than C functions, but this isn't always the case.
Kyle
Actually, there's minimal overhead in calling Obj-C methods vs. C functions, so I wouldn't worry about that. However, yes, these routines are much slower because on the iPhone they appear to use Webkit to do the text layout. Whether or not that becomes a problem for you depends on how often you re-render the text. I use it all throughout Pi Cubed, which has very good rendering performance: http://www.sunsetlakesoftware.com/picubed .
Brad Larson
Just for future reference, I did a simple benchmark that compared drawAtPoint to CGContextShowTextAtPoint. I drew 36 character strings 1000 times at random y-locations across the screen and here is what I got (on a 1g iPod touch): CGContextShowTextAtPoint - 99 refreshes/sec, drawAtPoint - 75 refreshes/sec. So, the CG method is clearly much faster.
Kyle
+1  A: 

You should first spend some quality time understanding how this problem was solved on Mac:

http://developer.apple.com/documentation/Cocoa/Conceptual/TextArchitecture/Tasks/AssembleSysByHand.html

http://developer.apple.com/documentation/Cocoa/Conceptual/TextLayout/TextLayout.html

In particular, you should become familiar with line fragment generation, which is the problem you're trying to solve for word-wrap, and you should understand glyph generation in order to do rich text well. You don't need to implement all of the flexibility of NSTypesetter or NSLayoutManager of course, but the text system on Mac is incredibly powerful, and you should learn from its example. Implementing something similar to NSAttributedString for iPhone may be valuable to you and improve your performance.

Unless you're moving things around a lot (for which this would best work in a UIScrollView), I don't think you should need to use CALayers here. That seems overkill for the problem, and may actually adversely impact the optimizations already provided by UIScrollView. If you're seeing performance problems, first make sure you're not doing redundant calculations within your drawRect:.

Rob Napier
Thanks Rob, I will have a look at those documents. The only reason I was going to us CALayers was to get around the 1024 (or 2048) pixel height limit for views. I was going to use multiple layers to extend this.
Kyle
Regarding the view height limit, rather than render the entire view (eating an arbitrary amount of memory), I would have 5-7 views that are each the size of a screen and stack them in a UIScrollView. As the user scrolls, you swap the bottom ones to the top or vice versa.This is very similar to a UITableView, and you may want to consider just implementing this as a UITableView so that you get all of its display optimizations. The problem with UITableView is that there is no way to mark individual cells as dirty, so you have to reloadData when anything changes. No good if this is editable.
Rob Napier
I was planning something like this, and using the scroll views delegate methods to draw more of the text. In this case would layers be better than views because they are more lightweight, or is the difference negligible. It is editable, so table views are out.
Kyle
+1  A: 

Check out TTStyledText in Three20 library. Not sure how well it matches your goals, but might serve you as an example. (The library itself is a bit bloated, but is a wonderful source to look at.)

Andrey Tarantsov
+1 for Three20. Josh is a demon. That codebase is a wonder to behold and a nightmare to trace. : )
Genericrich
A: 

Kyle, not sure if you'll see this, but I posted a question that you'd probably be able to answer -- would definitely appreciate your insight: link text

SeanG