NSLayoutManager numberOfGlyphs always showing 0, Swift - ios

I am trying to create a multipage PDF with UITextView on each page with the required attributed text. but When I am hitting a problem once the app is Archived and distributed via TestFlight for testing.
Below is a my sample codes which I used to generate the multi pages,
var textStorage = NSTextStorage()
textStorage = NSTextStorage(attributedString: attString)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
var pageSize = CGRect(x: 44, y: 108, width: 507, height: 690)
var lastGlyph = 0
while lastGlyph < layoutManager.numberOfGlyphs {
let textContainer = NSTextContainer()
let background = UINib(nibName: "background", bundle: nil).instantiate(withOwner: nil, options: nil)[0]
background.frame = pageRect
textContainer.size = subsequentPageSize.size
layoutManager.addTextContainer(textContainer)
let textView = UITextView(frame: pageSize, textContainer: textContainer)
pageSize.origin.x += pageSize.width
background.addSubview(textView)
context.beginPage()
background.layer.render(in: UIGraphicsGetCurrentContext()!)
lastGlyph = NSMaxRange(layoutManager.glyphRange(for: textContainer))
}
This works perfectly fine if run in the simulator or on device when built from Xcode but as soon as the app is distributed the layoutManager.numberOfGlyphs always returns 0 even if I print() the layoutmanager it shows,
<NSLayoutManager: 0x7ff313c9f6d0>
0 containers, text backing has 57 characters
Currently holding 57 glyphs.
Glyph tree contents: 57 characters, 57 glyphs, 1 nodes, 64 node bytes, 64 storage bytes, 128 total bytes, 2.25 bytes per character, 2.25 bytes per glyph
Layout tree contents: 57 characters, 57 glyphs, 0 laid glyphs, 0 laid line fragments, 1 nodes, 64 node bytes, 0 storage bytes, 64 total bytes, 1.12 bytes per character, 1.12 bytes per glyph, 0.00 laid glyphs per laid line fragment, 0.00 bytes per laid line fragment'.
Have I missed something silly or is there a bug that I am not aware of? I cannot for the life of me understand why it is not working!
Appreciate ay help that could be given.

I eventually found a solution in the NSTextStorage subclassing notes which state,
The NSTextStorage class implements change management (via the beginEditing() and endEditing() methods), verification of attributes, delegate handling, and layout management notification. The one aspect it does not implement is managing the actual attributed string storage, which subclasses manage by overriding the two NSAttributedString primitives...
So I have changed my code and added textStorage.beginEditing() and textStorage.endEditing() to the beginning and end of the entire sequence as follows and it now works once the project achieves as well as built directly to device or simulator from Xcode.
var textStorage = NSTextStorage()
textStorage.beginEditing()
if attribString != nil {
textStorage = NSTextStorage(attributedString: attribString)
} else {
textStorage = NSTextStorage(string: string)
}
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
var pageSize = CGRect(x: 44, y: 108, width: 507, height: 690)
var lastGlyph = 0
while lastGlyph < layoutManager.numberOfGlyphs {
let textContainer = NSTextContainer()
let background = UINib(nibName: "background", bundle: nil).instantiate(withOwner: nil, options: nil)[0]
background.frame = pageRect
textContainer.size = subsequentPageSize.size
layoutManager.addTextContainer(textContainer)
let textView = UITextView(frame: pageSize, textContainer: textContainer)
background.addSubview(textView)
context.beginPage()
background.layer.render(in: UIGraphicsGetCurrentContext()!)
lastGlyph = NSMaxRange(layoutManager.glyphRange(for: textContainer))
}
textStorage.endEditing()

Related

How to get height of string with maximum lines using NSTextStorage?

I am trying to get the height of an attributed string (for any font, any language, any strange utf8 characters, etc).
I found this interesting topic at Badoo Chatto about different solutions: https://github.com/badoo/Chatto/issues/129
And the solution I'm using is theirs:
func height(width: CGFloat, attributes: [NSAttributedString.Key: Any]) -> CGFloat {
let textContainer: NSTextContainer = {
let container = NSTextContainer(size: CGSize(width: width, height: .greatestFiniteMagnitude))
container.lineFragmentPadding = 0
return container
}()
let textStorage = NSTextStorage(string: self, attributes: attributes)
let layoutManager: NSLayoutManager = {
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
return layoutManager
}()
let rect = layoutManager.usedRect(for: textContainer)
return rect.size.round().height
}
How can I modify this logic so that it can take into consideration a maximum line number?
I tried adding container.maximumNumberOfLines = 2 but it won't change anything as NSTextContainer is set with an infinite height.
Ideally I would like to avoid using any UIView or subview as this processing has to be done in a background thread. Also, it appears that any UIKit-based solution isn't 100% reliable (cf the Badoo Chatto link).

Locate character coordinate in Custom UILabel with attributedText

i need to locate my character location in my UILabel (it has ParagraphLineSpacing and AttributedText with multiline),
i have got my character's index, but now i can't get X and Y coordinate from my index.
i Have found this http://techqa.info/programming/question/19417776/how-do-i-locate-the-cgrect-for-a-substring-of-text-in-a-uilabel
and i translated to my Swift 3.1 code
func boundingRect(forCharacterRange range: NSRange) -> CGRect {
let textStorage = NSTextStorage(attributedString: self.attributedText!)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: bounds.size)
textContainer.lineFragmentPadding = 0
layoutManager.addTextContainer(textContainer)
var glyphRange: NSRange
// Convert the range for glyphs.
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: glyphRange)
return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
}
but, unfortunately, i can't really use this code because actualGlyphRange ask NSRangePointer, not NSRange, so i changed my translated code to
func boundingRect(forCharacterRange range: NSRange) -> CGRect {
let textStorage = NSTextStorage(attributedString: self.attributedText!)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: bounds.size)
textContainer.lineFragmentPadding = 0
layoutManager.addTextContainer(textContainer)
//var glyphRange: NSRange
let a = MemoryLayout<NSRange>.size
let pointer:NSRangePointer = NSRangePointer.allocate(capacity: a)
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: pointer)
return layoutManager.boundingRect(forGlyphRange: range, in: textContainer)
}
i don't understand what
var glyphRange: NSrange
usage, so i removed it and now the code is working, but the result is 60% not accurate especially when my character located on the second line or the third line. Do i messed up the translation here? Or are there any better method to get my character coordinate accurately?
i use
NSMakeRange(index, 1)
for my params to locate one specific character
=======UPDATED=======
I have tried custom UITextView to access its layout Manager, but unfortunately, the position is still inaccurate if there are 2 lines or more. (only accurate if there is only 1 line in my textView)
class LyricTextView: UITextView {
func boundingRect(forCharacterRange range: NSRange) -> CGRect {
let inset = self.textContainerInset
let rect = self.layoutManager.boundingRect(forGlyphRange: range, in: textContainer).offsetBy(dx: inset.left, dy: inset.top)
return rect
}
}
Am i missing something in this new code? It is getting nearly done
An easier fix for your compilation error would be to use this:
var glyphRange = NSRange()
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
However, when I tried it, I also could only get correct rectangles for text on the first line.
If using a UITextView is ok for you, you have access to its layout manager and text container:
#IBOutlet var textView: UITextView!
...
let rect = textView!.layoutManager.boundingRect(
forGlyphRange: glyphRange, in: textView!.textContainer)
It seems you also need to take into account the text view's text container inset so the following worked for me to get a bounding rect for text on the second line:
let inset = textView!.textContainerInset
let rect = textView!.layoutManager.boundingRect(
forGlyphRange: glyphRange, in: textView!.textContainer)
.offsetBy(dx: inset.left, dy: inset.top)
I'd be interested if somebody finds a solution that works for UILabel.

Dynamically set UILabel text alignment between .left and .justified

In my app I have a UILabel with two lines preset. I can set the text alignment to either .left or .justified.
If I set it to .left, there is no layout issue if there is enough space between the last word in a line and the maximum x position of the label. Yet, when there is not so much space, so that the last word is very near the maximum x position, it looks kinda weird, because it is not exactly right-aligned (as it would be with .justified.
If I set it to .justified, it is always aligned well, yet sometimes the distance between the individual characters looks weird.
What I'm looking for is a way to dynamically adjust the text alignment depending on the distance between the last word in the first line to the maximum x position of the label. Say, if the position of the last character of the last word is smaller than 50, I want to have text alignment .left, otherwise I'd like to have .justified. Is there any way on how to accomplish this?
I took a quite hacky approach which takes some processing power, but it seems to work.
First of all, I fetch the string in the first line of the label using this extension:
import CoreText
extension UILabel {
/// Returns the String displayed in the first line of the UILabel or "" if text or font is missing
var firstLineString: String {
guard let text = self.text else { return "" }
guard let font = self.font else { return "" }
let rect = self.frame
let attStr = NSMutableAttributedString(string: text)
attStr.addAttribute(String(kCTFontAttributeName), value: CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil), range: NSMakeRange(0, attStr.length))
let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
let path = CGMutablePath()
path.addRect(CGRect(x: 0, y: 0, width: rect.size.width + 7, height: 100))
let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
guard let line = (CTFrameGetLines(frame) as! [CTLine]).first else { return "" }
let lineString = text[text.startIndex...text.index(text.startIndex, offsetBy: CTLineGetStringRange(line).length-2)]
return lineString
}
}
After that I calculate the width, a label with line number 1 and fixed height would require for that string using this extension:
extension UILabel {
/// Get required width for a UILabel depending on its text content and font configuration
class func calculateWidth(text: String, height: CGFloat, font: UIFont) -> CGFloat {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: CGFloat.greatestFiniteMagnitude, height: height))
label.numberOfLines = 1
label.font = font
label.text = text
label.sizeToFit()
return label.frame.size.width
}
}
Based on that, I can calculate the distance to the right and decide whether to choose text alignment .left or .justified, so the main code looks like this:
// Set text
myLabel.text = someString
// Change text alignment depending on distance to right
let firstLineString = myLabel.firstLineString
let distanceToRight = myLabel.frame.size.width - UILabel.calculateWidth(text: firstLineString, height: myLabel.frame.size.height, font: myLabel.font)
myLabel.textAlignment = distanceToRight < 20 ? .justified : .left

NSLayoutManager, boundingRectForGlyphRange and emoji

I am displaying text that may contain emoji using NSAttributedString's drawInRect(rect: CGRect) method. Since I want to detect taps on the text I use the following method to see which character has been tapped:
let textStorage = NSTextStorage(attributedString: attributedString)
let layoutManager = NSLayoutManager()
layoutManager.usesFontLeading = true
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: containerSize)
layoutManager.addTextContainer(textContainer)
textContainer.lineFragmentPadding = 0.0
layoutManager.ensureLayoutForTextContainer(textContainer)
let tappedIndex = layoutManager.characterIndexForPoint(point,
inTextContainer: textContainer,
fractionOfDistanceBetweenInsertionPoints: nil)
This gives the correct index that I can work with until I start adding emoji to the text. As soon as emoji are added there starts to be an offset for the detection. This led me to look at the bounding rectangles of glyphs that I was looking for. I noticed that the bounding rectangles of emoji were too large. I set up the following test case to check the difference:
let emojiText = "😀"
let font = UIFont.systemFontOfSize(20.0)
let containerSize = CGSize(width: 300.0, height: 20000.0)
let attributedString = NSAttributedString(string: emojiText, attributes: [NSFontAttributeName: font])
let textStorage = NSTextStorage(attributedString: attributedString)
let layoutManager = NSLayoutManager()
layoutManager.usesFontLeading = true
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: containerSize)
layoutManager.addTextContainer(textContainer)
textContainer.lineFragmentPadding = 0.0
layoutManager.ensureLayoutForTextContainer(textContainer)
let glyphRect = layoutManager.boundingRectForGlyphRange(NSRange(location: 0, length: attributedString.length), inTextContainer: textContainer)
let boundingRect = attributedString.boundingRectWithSize(containerSize, options:[.UsesLineFragmentOrigin, .UsesFontLeading], context: nil)
Executing this code resulted the following CGRects:
glyphRect = (0.0, 0.0, 23.0, 28.875)
boundingRect = (0.0, 0.0, 23.0, 23.8671875)
What this means is that these two methods give two entirely different sizes! This wouldn't be a problem, but the 'offset' stacks with more lines.
Example of the stacked offset
I set a purple background for the character the characterIndexForPoint gave me, gave the rect of boundingRectForGlyphRange a green outline and the yellow dot is the actual taplocation. Note that the green rectangle lines up nicely with a different character, however, this is no indication whatsoever, since it just happens to line up nicely in this specific case.
Am I overlooking something obvious or is this an issue in iOS?
I have solved the issue. It appears that NSAttributedString.drawInRect draws differently from CoreText. I now use the following code to draw the text in drawRect:
let totalRange = layoutManager.glyphRangeForTextContainer(textContainer)
layoutManager.drawBackgroundForGlyphRange(range, atPoint: CGPointZero)
layoutManager.drawGlyphsForGlyphRange(range, atPoint: CGPointZero)

boundingRectForGlyphRange always returns rect for font size 12

I am trying to calculate the rects of individual glyphs using boundingRectForGlyphRange method. Everything works well except it always returns the rect as if the font size was 12. Event though I set it to 20 in attributed string.
let font = UIFont.systemFontOfSize(20)
var attributedString = NSMutableAttributedString(string: text)
let range = NSRange(location: 0, length: countElements(text))
attributedString.addAttribute(NSFontAttributeName, value: font, range: range)
let textContainer = NSTextContainer()
textContainer.size = CGSize(width: 300, height: 100)
let layoutManager = NSLayoutManager()
let textStorage = NSTextStorage(string: attributedString.mutableString)
layoutManager.textStorage = textStorage
layoutManager.addTextContainer(textContainer)
let rect = layoutManager.boundingRectForGlyphRange(NSRange(location: 0, length: 1), inTextContainer: textContainer)
How do I make it return rect for text size 20 instead of 12?
Just initialise NSTextStorage with attributedString:.
let textStorage = NSTextStorage(attributedString: attributedString)
instead of
let textStorage = NSTextStorage(string: attributedString.mutableString)

Resources