1

I'm working on rendering text in the center of a circular or oval shaped region. Ideally, I'd like to center the text vertically and horizontally, but have it flow naturally at the boundaries of the region.

I've found an approach using a UITextView and NSTextContainer with exclusion paths that does a good job of laying out the text at a given vertical offset, centered horizontally (code below). However, I'm not sure how to achieve vertical centering.

The approaches I've seen all suggest adjusting the contentInset of the text view after the text has been laid out and the final height is known. (For example, this question) However, in the case of an irregularly shaped region, the height of the text after it's been laid out will depend on where in the region layout starts from.

An approach I've considered is to retry the layout process until a satisfactory layout is achieved. Has anyone had success with this kind of approach? One challenge here is that I haven't worked out how to determine whether all the text has been rendered within the view (i.e. whether there is enough space to lay out all the text) --- is there a way to query whether all content has been 'rendered' when using a UITextView?

Finally: this is just for displaying text --- there is no need to allow a user to edit the content of the view. Would CoreText perhaps be a better approach in this case?

I'm fairly new to iOS development, so if there's anything outrageous I'm doing, that would also be helpful to know!

Thanks!

let boundingRect = CGRect(...)
let textView = UITextView(frame: boundingRect)

textView.editable = false
view.addSubview(textView)

let verticalInset:CGFloat = <some value>

let width = boundingRect.width
let height = boundingRect.height
let textBounds = CGSize(width: width, height:height - 2*verticalInset)
textView.bounds = textBounds

let exclusionRect = CGRect(x:0, y:-verticalInset, width:width, height:height)

let textRegion = UIBezierPath(ovalInRect: exclusionRect)    
let exclusionPath = UIBezierPath(rect:exclusionRect)
exclusionPath.appendPath(textRegion.bezierPathByReversingPath())

textView.textContainer.exclusionPaths = [exclusionPath]

let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .Center

let attributes = [NSParagraphStyleAttributeName: paragraphStyle]
let formattedContent = NSAttributedString(string: "....", attributes:attributes)

textView.attributedText(formattedContent)

Here's an image to help visualize the above, if helpful: example

Community
  • 1
  • 1
duncanm
  • 796
  • 1
  • 6
  • 13

2 Answers2

2

If you lay out text in a rectangular area, it's easy to center it vertically afterwards by shifting it upwards or downwards. If the area is not rectangular, as in your case, this is not possible, as the text doesn't necessarily fit in the shape after you shift it vertically.

I believe that a solution to this is always an iterative method. For example: Render the text in the shape with increasing insets, and find the largest inset so that the text fits in the resulting shape.

To do this with Core Text, you can create a CTFrame for the text layout like this:

let text:NSAttributedString = ... // The text to render
let path:CGPath = ... // Bounds the shape in which the text is rendered
let framesetter = CTFramesetterCreateWithAttributedString(text)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)

With this frame, you can check if the whole string has been rendered:

let range = CTFrameGetVisibleStringRange(frame)
if text.length == range.length {
  // Fits
}

And to render the text:

let ctx:CGContext = ... 
CTFrameDraw(renderFrame, ctx)

If you need more details about the rendered text, you can use CTFrameGetLines to get the CTLines from frame. Then, using CTFrameGetLineOrigins and CTLineGetBoundsWithOptions with the option UseGlyphPathBounds or UseOpticalBounds, you can calculate the optical bounds for the single lines. This allows for a more precise vertical adjustment. (Brace yourself for some linear algebra, the coordinate system transformations that are needed to use CoreText with CoreGraphics directly can be a bit tedious).

Theo
  • 3,826
  • 30
  • 59
0

I can't offer a fantastic answer here.

However, I've just tried out YYText. This has a "YYLabel" that supports vertical text alignment, and exclusion paths. Take a look at their Demo project, specifically the 'TextAttributes1' row in their tableview that demonstrates.

  • Thanks very much for the suggestion @Christopher. I'll check that out. Actually --- I also managed to find a workable solution for my usecase - not pretty, but fine for now - which I'll post below. Cheers! – duncanm Apr 08 '16 at 11:22
  • Oop - should add: once I've had a chance to check out YYText, will mark your answer. – duncanm Apr 08 '16 at 15:24