String: Minimum bounds for maximum height

1.1k views Asked by At

I want to calculate the minimum bounds for a rectangle needed to fit a string (multi line) with a specific font.

It should look something like this:

FROM:

 ----------------------------------
|Sent when the application is about|
|to move from active to inactive   |
|state.                            |
 ----------------------------------

TO:

 -------------------------
|Sent when the application|
|is about to move from    |
|active to inactive state.|
 -------------------------

As you can see, the height stays the same, the width changes to the minimum value necessary.

Initially I though I could use boundingRectWithSize (constraint to maximum width) to first get the minimum height needed and then call boundingRectWithSize (constraint to calculated height) the get the width. But this produces wrong results when calculating the width in the second step. It does not consider the max height but simple calculates the width for a single line string.

After that I found a way to get the proper result but executing this code takes really long which makes it of no use to me:

First calculate the needed rect for constraint width:

var objectFrame = Class.sizeOfString(string, font: objectFont, width: Double(width), height: DBL_MAX)

then the width:

objectFrame.size.width = Class.minWidthForHeight(string, font: objectFont, objectFrame.size.height)

using:

class func minWidthForHeight(string: NSString, font: UIFont, height: CGFloat) -> CGFloat
{
    let deltaWidth: CGFloat = 5.0
    let neededHeight: CGFloat = rect.size.height
    var testingWidth: CGFloat = rect.size.width

    var done = false
    while (done == false)
    {
        testingWidth -= deltaWidth

        var newSize = Class.sizeOfString(string, font: font, width: Double(testingWidth), height: DBL_MAX)

        if (newSize.height > neededHeight)
        {
            testingWidth += deltaWidth
            done = true
        }
    }
    return testingWidth
}

class func sizeOfString(string: NSString, font: UIFont, width: Double, height: Double) -> CGRect
{
    return string.boundingRectWithSize(CGSize(width: width, height: height),
        options: NSStringDrawingOptions.UsesLineFragmentOrigin,
        attributes: [NSFontAttributeName: font],
        context: nil)
}

It gradually calculates the height for a given width (- 5.0 pixels each new step) and checks wether the height stays the same. As soon as the height changes, it returns the width of the previous step. So now we have a bounding rectangle where the string for a certain font fits perfectly without any wasted space.

But as I said, this takes a really long time to calculate, especially when doing it for many different strings simultaneously.

Is there a better and faster way to do this?

1

There are 1 answers

9
freshking On BEST ANSWER

So this code below is my final approach to to question at hand. It's pretty fast and works well for me. Thanks to @Nero for getting me on the right track.

class func minFrameWidthForHeight(string: NSString, font: UIFont, rect: CGRect) -> CGFloat
{
    if (string.componentsSeparatedByCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()).count <= 1)
    {
        return rect.size.width
    }

    var minValue: CGFloat = rect.size.width / 2
    var maxValue: CGFloat = rect.size.width

    var testingWidth: CGFloat = rect.size.width
    var lastTestingWidth: CGFloat = testingWidth
    let neededHeight: CGFloat = rect.size.height

    var newSize = rect

    while (newSize.height <= neededHeight)
    {
        lastTestingWidth = testingWidth

        testingWidth = (maxValue + minValue) / 2

        newSize = CalculationHelper.sizeOfString(string, font: font, width: Double(testingWidth), height: DBL_MAX)

        if (newSize.height <= neededHeight)
        {
            maxValue = testingWidth
        }
        else
        {
            minValue = testingWidth
        }
    }

    return lastTestingWidth
}