5

I noticed that iOS 7 introduces new classes related to text layout such as NSLayoutManager, NSTextStorage, and NSTextContainer. How can I use these in order to get information about word wrapping on an NSString?

For example, say I have a long NSString which I put in a UILabel. If I enable multiple lines on the UILabel, it would produce a string such as the following:

The quick brown fox jumps
over the lazy dog.

That's great, but I can't access the line breaks in code (e.g. after the word jumps I would want it to return \n or something similar). I would want to know at which character indexes the line breaks occur. I know we can do this with CoreText, but since we have these new classes in iOS 7, I was wondering how we can use them instead.

Community
  • 1
  • 1
Senseful
  • 86,719
  • 67
  • 308
  • 465

3 Answers3

14

Example:

CGFloat maxWidth = 150;
NSAttributedString *s = 
    [[NSAttributedString alloc] 
        initWithString:@"The quick brown fox jumped over the lazy dog." 
        attributes:@{NSFontAttributeName:[UIFont fontWithName:@"GillSans" size:20]}];
NSTextContainer* tc = 
    [[NSTextContainer alloc] initWithSize:CGSizeMake(maxWidth,CGFLOAT_MAX)];
NSLayoutManager* lm = [NSLayoutManager new];
NSTextStorage* tm = [[NSTextStorage alloc] initWithAttributedString:s];
[tm addLayoutManager:lm];
[lm addTextContainer:tc];
[lm enumerateLineFragmentsForGlyphRange:NSMakeRange(0,lm.numberOfGlyphs) 
    usingBlock:^(CGRect rect, CGRect usedRect, 
                 NSTextContainer *textContainer, 
                 NSRange glyphRange, BOOL *stop) {
    NSRange r = [lm characterRangeForGlyphRange:glyphRange actualGlyphRange:nil];
    NSLog(@"%@", [s.string substringWithRange:r]);
}];
Senseful
  • 86,719
  • 67
  • 308
  • 465
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    It seems like you should include the line `tc.lineFragmentPadding = 0` as well, otherwise it will not line up with a UILabel properly. – Senseful Jan 03 '14 at 01:10
  • I was looking for exactly this, but maybe I'm missing something. I can't actually access this information. The NSLog output is inside the enumeratedLineFragmentsForGlyphRange. I can't seem to, lats say, transfer each line to an array. Thoughts? – russellaugust May 16 '14 at 05:30
3

swift translation:

    guard let font1: UIFont = textView.font else { return }
    var lines: [String] = []

    let maxWidth: CGFloat = textView.frame.width
    let s: NSAttributedString = NSAttributedString.init(string: textView.text, attributes: [.font: font1])
    let tc: NSTextContainer = NSTextContainer.init(size: CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude))
    let lm: NSLayoutManager = NSLayoutManager.init()
    let tm: NSTextStorage = NSTextStorage.init(attributedString: s)
    tm.addLayoutManager(lm)
    lm.addTextContainer(tc)
    lm.enumerateLineFragments(forGlyphRange: NSRange(location: 0, length: lm.numberOfGlyphs)) { (rect: CGRect, usedRect: CGRect, textContainer: NSTextContainer, glyphRange: NSRange, Bool) in

        let r: NSRange = lm.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)

        let str = s as NSAttributedString

        let s2 = str.attributedSubstring(from: r)

        print(s2)

        lines.append(s2.string)

    }
brontea
  • 555
  • 4
  • 14
1

I created an extesion of UITextView with the answer of @brontea, it returns the text of the textView formatted with line breaks to save the string the same way the user sees it:

extension UITextView {

   func textFormattedWithLineBreaks() -> String? {
    guard let font: UIFont = self.font else { return "" }
    var textPerLine: [String] = []
    
    let maxWidth: CGFloat = self.frame.width
    let attributedString: NSAttributedString = NSAttributedString.init(string: self.text, attributes: [.font: font])
    let textContainer: NSTextContainer = NSTextContainer.init(size: CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude))
    let layoutManager: NSLayoutManager = NSLayoutManager.init()
    let textStorage: NSTextStorage = NSTextStorage.init(attributedString: attributedString)
    textStorage.addLayoutManager(layoutManager)
    layoutManager.addTextContainer(textContainer)
    layoutManager.enumerateLineFragments(forGlyphRange: NSRange(location: 0, length: layoutManager.numberOfGlyphs)) { (rect: CGRect, usedRect: CGRect, textContainer: NSTextContainer, glyphRange: NSRange, Bool) in
        
        let range: NSRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
        
        let string = attributedString as NSAttributedString
        
        let stringWithRange = string.attributedSubstring(from: range)
        
        print(stringWithRange.string)
        
        textPerLine.append(stringWithRange.string)
    }
    
    return textPerLine.map{ "\($0)\n" }.joined()
    
   }
}
Alessandro Pace
  • 206
  • 4
  • 8