Skip to content

Instantly share code, notes, and snippets.

@algal
Created September 25, 2015 17:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save algal/97cd780281181e8a3ffa to your computer and use it in GitHub Desktop.
Save algal/97cd780281181e8a3ffa to your computer and use it in GitHub Desktop.
Playground gist showing a bug in how NSAttributedString misinterprets LINE SEPARATOR
//: Playground - noun: a place where people can play
import UIKit
/*:
This playground shows a bug in how `NSAttributedString.boundingRectWithSize(_:options:context:)` handles attributed strings containing the LINE SEPARATOR character when the .UsesDeviceMetrics option is not included.
When the `.UsesDeviceMetrics` option is set, the function is supposed to return the bounding rect where the height is what is required to bound the particular glyphs in the string.
When the `.UsesDeviceMetrics` option is not set, the function is supposed to return the bounding rect where the height is what is required to bound the font's generic line height and line gap.
The Unicode LINE SEPARATOR character (U+2028) is supposed to produce a vertical gap between lines based on the line spacing, not the `NSParagraphStyle.paragraphSpacing`. Therefore, a LINE SEPARATOR should never on its own yield a PARAGRAPH SPACING.
But we can see below is that when .UsesDeviceMetrics is false, the function uses a paragraph spacing when it sees a LINE SEPARATOR.
*/
let SPACE:Character = " "
let LINE_SEPARATOR:Character = "\u{2028}"
let PARAGRAPH_SEPARATOR:Character = "\u{2029}"
let f = UIFont(name: "HelveticaNeue-Medium", size: 16)!
let longWord = "Antidisestablishmentarianism"
/// width which will force lines to break between at characters separating `longWord`s
let lineBreakingWidth = NSAttributedString(string:longWord, attributes: [NSFontAttributeName:f]).boundingRectWithSize(CGSizeMake(1000,1000), options: [], context:nil).size.width * 1.2
/// Returns an attributed string with long words broken by `character`
func textWithWordBreakCharacter(character:Character) -> NSAttributedString
{
let bigParaStyle = NSMutableParagraphStyle()
bigParaStyle.paragraphSpacing = 300
let atts = [
NSFontAttributeName:f,
NSParagraphStyleAttributeName:bigParaStyle
]
let text = Repeat(count: 2, repeatedValue: longWord).joinWithSeparator(String(character))
return NSAttributedString(string: text, attributes: atts)
}
/// Returns the height required when the string is not forced to wrap by its containing rect
func intrinsicHeightOfText(text:NSAttributedString,deviceMetrics:Bool) -> CGFloat
{
var options:NSStringDrawingOptions = [.UsesFontLeading, .UsesLineFragmentOrigin]
if deviceMetrics { options.insert(.UsesDeviceMetrics) }
return text.boundingRectWithSize(CGSize(width: 9000, height: 9000), options: options, context:nil).size.height
}
/// Returns the height required when the string wraps to fit within a rect
func wrappedHeightOfText(text:NSAttributedString,deviceMetrics:Bool) -> CGFloat
{
/// width which will force lines to break between at characters separating `longWord`s
let lineBreakingWidth = NSAttributedString(string:longWord, attributes: [NSFontAttributeName:f]).boundingRectWithSize(CGSizeMake(9000,9000), options: [], context:nil).size.width * 1.2
var options:NSStringDrawingOptions = [.UsesFontLeading, .UsesLineFragmentOrigin]
if deviceMetrics { options.insert(.UsesDeviceMetrics) }
return text.boundingRectWithSize(CGSize(width: lineBreakingWidth, height: 0), options: options, context:nil).size.height
}
/// Returns a Bool pair indicating if the intrinsic height is correctly using the line spacing (as opposed to the paragraph spacing) and if the wrapped height is similarly correct
func usingCorrectLineSpacingForBreakCharacter(character:Character, deviceMetrics:Bool) -> (Bool,Bool)
{
let text = textWithWordBreakCharacter(character)
let heights = (
intrinsicHeightOfText(text, deviceMetrics: deviceMetrics),
wrappedHeightOfText(text, deviceMetrics: deviceMetrics)
)
return (heights.0 < 100,heights.1 < 100)
}
usingCorrectLineSpacingForBreakCharacter(SPACE, deviceMetrics: true)
usingCorrectLineSpacingForBreakCharacter(LINE_SEPARATOR, deviceMetrics: true)
usingCorrectLineSpacingForBreakCharacter(SPACE, deviceMetrics: false)
usingCorrectLineSpacingForBreakCharacter(LINE_SEPARATOR, deviceMetrics: false)
/*:
Returned results as of iOS9 on 2015-09-25:
Break Character | Device Metrics | => ( correct intrinsic height, correct wrapped height )
---------------------------------------------------------------------------------------------
SPACE | true | ( true , true )
LINE SEPARATOR | true | ( true , true )
SPACE | false | ( true , true )
LINE SEPARATOR | false | ( false , false )
This bug may be what is causing UILabel to process LINE SEPARATOR incorrectly.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment